diskstore.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. // Copyright (C) 2023 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. package main
  7. import (
  8. "bytes"
  9. "compress/gzip"
  10. "context"
  11. "io"
  12. "log"
  13. "os"
  14. "path/filepath"
  15. "sort"
  16. "time"
  17. )
  18. type diskStore struct {
  19. dir string
  20. inbox chan diskEntry
  21. maxBytes int64
  22. maxFiles int
  23. currentFiles []currentFile
  24. currentSize int64
  25. }
  26. type diskEntry struct {
  27. path string
  28. data []byte
  29. }
  30. type currentFile struct {
  31. path string
  32. size int64
  33. mtime int64
  34. }
  35. func (d *diskStore) Serve(ctx context.Context) {
  36. if err := os.MkdirAll(d.dir, 0o700); err != nil {
  37. log.Println("Creating directory:", err)
  38. return
  39. }
  40. if err := d.inventory(); err != nil {
  41. log.Println("Failed to inventory disk store:", err)
  42. }
  43. d.clean()
  44. cleanTimer := time.NewTicker(time.Minute)
  45. inventoryTimer := time.NewTicker(24 * time.Hour)
  46. buf := new(bytes.Buffer)
  47. gw := gzip.NewWriter(buf)
  48. for {
  49. select {
  50. case entry := <-d.inbox:
  51. path := d.fullPath(entry.path)
  52. if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
  53. log.Println("Creating directory:", err)
  54. continue
  55. }
  56. buf.Reset()
  57. gw.Reset(buf)
  58. if _, err := gw.Write(entry.data); err != nil {
  59. log.Println("Failed to compress crash report:", err)
  60. continue
  61. }
  62. if err := gw.Close(); err != nil {
  63. log.Println("Failed to compress crash report:", err)
  64. continue
  65. }
  66. if err := os.WriteFile(path, buf.Bytes(), 0o600); err != nil {
  67. log.Printf("Failed to write %s: %v", entry.path, err)
  68. _ = os.Remove(path)
  69. continue
  70. }
  71. d.currentSize += int64(buf.Len())
  72. d.currentFiles = append(d.currentFiles, currentFile{
  73. size: int64(len(entry.data)),
  74. path: path,
  75. })
  76. case <-cleanTimer.C:
  77. d.clean()
  78. case <-inventoryTimer.C:
  79. if err := d.inventory(); err != nil {
  80. log.Println("Failed to inventory disk store:", err)
  81. }
  82. case <-ctx.Done():
  83. return
  84. }
  85. }
  86. }
  87. func (d *diskStore) Put(path string, data []byte) bool {
  88. select {
  89. case d.inbox <- diskEntry{
  90. path: path,
  91. data: data,
  92. }:
  93. return true
  94. default:
  95. return false
  96. }
  97. }
  98. func (d *diskStore) Get(path string) ([]byte, error) {
  99. path = d.fullPath(path)
  100. bs, err := os.ReadFile(path)
  101. if err != nil {
  102. return nil, err
  103. }
  104. gr, err := gzip.NewReader(bytes.NewReader(bs))
  105. if err != nil {
  106. return nil, err
  107. }
  108. defer gr.Close()
  109. return io.ReadAll(gr)
  110. }
  111. func (d *diskStore) Exists(path string) bool {
  112. path = d.fullPath(path)
  113. _, err := os.Lstat(path)
  114. return err == nil
  115. }
  116. func (d *diskStore) clean() {
  117. for len(d.currentFiles) > 0 && (len(d.currentFiles) > d.maxFiles || d.currentSize > d.maxBytes) {
  118. f := d.currentFiles[0]
  119. log.Println("Removing", f.path)
  120. if err := os.Remove(f.path); err != nil {
  121. log.Println("Failed to remove file:", err)
  122. }
  123. d.currentFiles = d.currentFiles[1:]
  124. d.currentSize -= f.size
  125. }
  126. var oldest time.Duration
  127. if len(d.currentFiles) > 0 {
  128. oldest = time.Since(time.Unix(d.currentFiles[0].mtime, 0)).Truncate(time.Minute)
  129. }
  130. log.Printf("Clean complete: %d files, %d MB, oldest is %v ago", len(d.currentFiles), d.currentSize>>20, oldest)
  131. }
  132. func (d *diskStore) inventory() error {
  133. d.currentFiles = nil
  134. d.currentSize = 0
  135. err := filepath.Walk(d.dir, func(path string, info os.FileInfo, err error) error {
  136. if err != nil {
  137. return err
  138. }
  139. if info.IsDir() {
  140. return nil
  141. }
  142. if filepath.Ext(path) != ".gz" {
  143. return nil
  144. }
  145. d.currentSize += info.Size()
  146. d.currentFiles = append(d.currentFiles, currentFile{
  147. path: path,
  148. size: info.Size(),
  149. mtime: info.ModTime().Unix(),
  150. })
  151. return nil
  152. })
  153. sort.Slice(d.currentFiles, func(i, j int) bool {
  154. return d.currentFiles[i].mtime < d.currentFiles[j].mtime
  155. })
  156. var oldest time.Duration
  157. if len(d.currentFiles) > 0 {
  158. oldest = time.Since(time.Unix(d.currentFiles[0].mtime, 0)).Truncate(time.Minute)
  159. }
  160. log.Printf("Inventory complete: %d files, %d MB, oldest is %v ago", len(d.currentFiles), d.currentSize>>20, oldest)
  161. return err
  162. }
  163. func (d *diskStore) fullPath(path string) string {
  164. return filepath.Join(d.dir, path[0:2], path[2:]) + ".gz"
  165. }