file_cache.go 2.6 KB

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