walk.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. package scanner
  2. import (
  3. "bytes"
  4. "errors"
  5. "io/ioutil"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "time"
  10. "github.com/calmh/syncthing/lamport"
  11. "github.com/calmh/syncthing/protocol"
  12. )
  13. type Walker struct {
  14. // Dir is the base directory for the walk
  15. Dir string
  16. // BlockSize controls the size of the block used when hashing.
  17. BlockSize int
  18. // If IgnoreFile is not empty, it is the name used for the file that holds ignore patterns.
  19. IgnoreFile string
  20. // If TempNamer is not nil, it is used to ignore tempory files when walking.
  21. TempNamer TempNamer
  22. // If CurrentFiler is not nil, it is queried for the current file before rescanning.
  23. CurrentFiler CurrentFiler
  24. // If Suppressor is not nil, it is queried for supression of modified files.
  25. // Suppressed files will be returned with empty metadata and the Suppressed flag set.
  26. // Requires CurrentFiler to be set.
  27. Suppressor Suppressor
  28. }
  29. type TempNamer interface {
  30. // Temporary returns a temporary name for the filed referred to by filepath.
  31. TempName(path string) string
  32. // IsTemporary returns true if path refers to the name of temporary file.
  33. IsTemporary(path string) bool
  34. }
  35. type Suppressor interface {
  36. // Supress returns true if the update to the named file should be ignored.
  37. Suppress(name string, fi os.FileInfo) (bool, bool)
  38. }
  39. type CurrentFiler interface {
  40. // CurrentFile returns the file as seen at last scan.
  41. CurrentFile(name string) File
  42. }
  43. // Walk returns the list of files found in the local repository by scanning the
  44. // file system. Files are blockwise hashed.
  45. func (w *Walker) Walk() (files []File, ignore map[string][]string, err error) {
  46. if debug {
  47. l.Debugln("Walk", w.Dir, w.BlockSize, w.IgnoreFile)
  48. }
  49. err = checkDir(w.Dir)
  50. if err != nil {
  51. return
  52. }
  53. t0 := time.Now()
  54. ignore = make(map[string][]string)
  55. hashFiles := w.walkAndHashFiles(&files, ignore)
  56. filepath.Walk(w.Dir, w.loadIgnoreFiles(w.Dir, ignore))
  57. filepath.Walk(w.Dir, hashFiles)
  58. if debug {
  59. t1 := time.Now()
  60. d := t1.Sub(t0).Seconds()
  61. l.Debugf("Walk in %.02f ms, %.0f files/s", d*1000, float64(len(files))/d)
  62. }
  63. err = checkDir(w.Dir)
  64. return
  65. }
  66. // CleanTempFiles removes all files that match the temporary filename pattern.
  67. func (w *Walker) CleanTempFiles() {
  68. filepath.Walk(w.Dir, w.cleanTempFile)
  69. }
  70. func (w *Walker) loadIgnoreFiles(dir string, ign map[string][]string) filepath.WalkFunc {
  71. return func(p string, info os.FileInfo, err error) error {
  72. if err != nil {
  73. return nil
  74. }
  75. rn, err := filepath.Rel(dir, p)
  76. if err != nil {
  77. return nil
  78. }
  79. if pn, sn := filepath.Split(rn); sn == w.IgnoreFile {
  80. pn := strings.Trim(pn, "/")
  81. bs, _ := ioutil.ReadFile(p)
  82. lines := bytes.Split(bs, []byte("\n"))
  83. var patterns []string
  84. for _, line := range lines {
  85. if len(line) > 0 {
  86. patterns = append(patterns, string(line))
  87. }
  88. }
  89. ign[pn] = patterns
  90. }
  91. return nil
  92. }
  93. }
  94. func (w *Walker) walkAndHashFiles(res *[]File, ign map[string][]string) filepath.WalkFunc {
  95. return func(p string, info os.FileInfo, err error) error {
  96. if err != nil {
  97. if debug {
  98. l.Debugln("error:", p, info, err)
  99. }
  100. return nil
  101. }
  102. rn, err := filepath.Rel(w.Dir, p)
  103. if err != nil {
  104. if debug {
  105. l.Debugln("rel error:", p, err)
  106. }
  107. return nil
  108. }
  109. if rn == "." {
  110. return nil
  111. }
  112. if w.TempNamer != nil && w.TempNamer.IsTemporary(rn) {
  113. // A temporary file
  114. if debug {
  115. l.Debugln("temporary:", rn)
  116. }
  117. return nil
  118. }
  119. if _, sn := filepath.Split(rn); sn == w.IgnoreFile {
  120. // An ignore-file; these are ignored themselves
  121. if debug {
  122. l.Debugln("ignorefile:", rn)
  123. }
  124. return nil
  125. }
  126. if w.ignoreFile(ign, rn) {
  127. // An ignored file
  128. if debug {
  129. l.Debugln("ignored:", rn)
  130. }
  131. if info.IsDir() {
  132. return filepath.SkipDir
  133. }
  134. return nil
  135. }
  136. if info.Mode().IsDir() {
  137. if w.CurrentFiler != nil {
  138. cf := w.CurrentFiler.CurrentFile(rn)
  139. if cf.Modified == info.ModTime().Unix() && cf.Flags == uint32(info.Mode()&os.ModePerm|protocol.FlagDirectory) {
  140. if debug {
  141. l.Debugln("unchanged:", cf)
  142. }
  143. *res = append(*res, cf)
  144. } else {
  145. f := File{
  146. Name: rn,
  147. Version: lamport.Default.Tick(0),
  148. Flags: uint32(info.Mode()&os.ModePerm) | protocol.FlagDirectory,
  149. Modified: info.ModTime().Unix(),
  150. }
  151. if debug {
  152. l.Debugln("dir:", cf, f)
  153. }
  154. *res = append(*res, f)
  155. }
  156. return nil
  157. }
  158. }
  159. if info.Mode().IsRegular() {
  160. if w.CurrentFiler != nil {
  161. cf := w.CurrentFiler.CurrentFile(rn)
  162. if cf.Flags&protocol.FlagDeleted == 0 && cf.Modified == info.ModTime().Unix() && cf.Flags == uint32(info.Mode()&os.ModePerm) {
  163. if debug {
  164. l.Debugln("unchanged:", cf)
  165. }
  166. *res = append(*res, cf)
  167. return nil
  168. }
  169. if w.Suppressor != nil {
  170. if cur, prev := w.Suppressor.Suppress(rn, info); cur && !prev {
  171. l.Infof("Changes to %q are being temporarily suppressed because it changes too frequently.", p)
  172. cf.Suppressed = true
  173. cf.Version++
  174. if debug {
  175. l.Debugln("suppressed:", cf)
  176. }
  177. *res = append(*res, cf)
  178. return nil
  179. } else if prev && !cur {
  180. l.Infof("Changes to %q are no longer suppressed.", p)
  181. }
  182. }
  183. }
  184. fd, err := os.Open(p)
  185. if err != nil {
  186. if debug {
  187. l.Debugln("open:", p, err)
  188. }
  189. return nil
  190. }
  191. defer fd.Close()
  192. t0 := time.Now()
  193. blocks, err := Blocks(fd, w.BlockSize)
  194. if err != nil {
  195. if debug {
  196. l.Debugln("hash error:", rn, err)
  197. }
  198. return nil
  199. }
  200. if debug {
  201. t1 := time.Now()
  202. l.Debugln("hashed:", rn, ";", len(blocks), "blocks;", info.Size(), "bytes;", int(float64(info.Size())/1024/t1.Sub(t0).Seconds()), "KB/s")
  203. }
  204. f := File{
  205. Name: rn,
  206. Version: lamport.Default.Tick(0),
  207. Size: info.Size(),
  208. Flags: uint32(info.Mode()),
  209. Modified: info.ModTime().Unix(),
  210. Blocks: blocks,
  211. }
  212. *res = append(*res, f)
  213. }
  214. return nil
  215. }
  216. }
  217. func (w *Walker) cleanTempFile(path string, info os.FileInfo, err error) error {
  218. if err != nil {
  219. return err
  220. }
  221. if info.Mode()&os.ModeType == 0 && w.TempNamer.IsTemporary(path) {
  222. os.Remove(path)
  223. }
  224. return nil
  225. }
  226. func (w *Walker) ignoreFile(patterns map[string][]string, file string) bool {
  227. first, last := filepath.Split(file)
  228. for prefix, pats := range patterns {
  229. if len(prefix) == 0 || prefix == first || strings.HasPrefix(first, prefix+"/") {
  230. for _, pattern := range pats {
  231. if match, _ := filepath.Match(pattern, last); match {
  232. return true
  233. }
  234. }
  235. }
  236. }
  237. return false
  238. }
  239. func checkDir(dir string) error {
  240. if info, err := os.Stat(dir); err != nil {
  241. return err
  242. } else if !info.IsDir() {
  243. return errors.New(dir + ": not a directory")
  244. }
  245. return nil
  246. }