file_cache.go 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. package server
  2. import (
  3. "errors"
  4. "fmt"
  5. "heckel.io/ntfy/util"
  6. "io"
  7. "os"
  8. "path/filepath"
  9. "regexp"
  10. "sync"
  11. "time"
  12. )
  13. var (
  14. fileIDRegex = regexp.MustCompile(fmt.Sprintf(`^[-_A-Za-z0-9]{%d}$`, messageIDLength))
  15. errInvalidFileID = errors.New("invalid file ID")
  16. errFileExists = errors.New("file exists")
  17. )
  18. type fileCache struct {
  19. dir string
  20. totalSizeCurrent int64
  21. totalSizeLimit int64
  22. fileSizeLimit int64
  23. mu sync.Mutex
  24. }
  25. func newFileCache(dir string, totalSizeLimit int64, fileSizeLimit int64) (*fileCache, error) {
  26. if err := os.MkdirAll(dir, 0700); err != nil {
  27. return nil, err
  28. }
  29. size, err := dirSize(dir)
  30. if err != nil {
  31. return nil, err
  32. }
  33. return &fileCache{
  34. dir: dir,
  35. totalSizeCurrent: size,
  36. totalSizeLimit: totalSizeLimit,
  37. fileSizeLimit: fileSizeLimit,
  38. }, nil
  39. }
  40. func (c *fileCache) Write(id string, in io.Reader, limiters ...util.Limiter) (int64, error) {
  41. if !fileIDRegex.MatchString(id) {
  42. return 0, errInvalidFileID
  43. }
  44. file := filepath.Join(c.dir, id)
  45. if _, err := os.Stat(file); err == nil {
  46. return 0, errFileExists
  47. }
  48. f, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
  49. if err != nil {
  50. return 0, err
  51. }
  52. defer f.Close()
  53. limiters = append(limiters, util.NewFixedLimiter(c.Remaining()), util.NewFixedLimiter(c.fileSizeLimit))
  54. limitWriter := util.NewLimitWriter(f, limiters...)
  55. size, err := io.Copy(limitWriter, in)
  56. if err != nil {
  57. os.Remove(file)
  58. return 0, err
  59. }
  60. if err := f.Close(); err != nil {
  61. os.Remove(file)
  62. return 0, err
  63. }
  64. c.mu.Lock()
  65. c.totalSizeCurrent += size
  66. c.mu.Unlock()
  67. return size, nil
  68. }
  69. func (c *fileCache) Remove(ids ...string) error {
  70. for _, id := range ids {
  71. if !fileIDRegex.MatchString(id) {
  72. return errInvalidFileID
  73. }
  74. file := filepath.Join(c.dir, id)
  75. _ = os.Remove(file) // Best effort delete
  76. }
  77. size, err := dirSize(c.dir)
  78. if err != nil {
  79. return err
  80. }
  81. c.mu.Lock()
  82. c.totalSizeCurrent = size
  83. c.mu.Unlock()
  84. return nil
  85. }
  86. // Expired returns a list of file IDs for expired files
  87. func (c *fileCache) Expired(olderThan time.Time) ([]string, error) {
  88. entries, err := os.ReadDir(c.dir)
  89. if err != nil {
  90. return nil, err
  91. }
  92. var ids []string
  93. for _, e := range entries {
  94. info, err := e.Info()
  95. if err != nil {
  96. continue
  97. }
  98. if info.ModTime().Before(olderThan) && fileIDRegex.MatchString(e.Name()) {
  99. ids = append(ids, e.Name())
  100. }
  101. }
  102. return ids, nil
  103. }
  104. func (c *fileCache) Size() int64 {
  105. c.mu.Lock()
  106. defer c.mu.Unlock()
  107. return c.totalSizeCurrent
  108. }
  109. func (c *fileCache) Remaining() int64 {
  110. c.mu.Lock()
  111. defer c.mu.Unlock()
  112. remaining := c.totalSizeLimit - c.totalSizeCurrent
  113. if remaining < 0 {
  114. return 0
  115. }
  116. return remaining
  117. }
  118. func dirSize(dir string) (int64, error) {
  119. entries, err := os.ReadDir(dir)
  120. if err != nil {
  121. return 0, err
  122. }
  123. var size int64
  124. for _, e := range entries {
  125. info, err := e.Info()
  126. if err != nil {
  127. return 0, err
  128. }
  129. size += info.Size()
  130. }
  131. return size, nil
  132. }