walk.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. // Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
  2. // All rights reserved. Use of this source code is governed by an MIT-style
  3. // license that can be found in the LICENSE file.
  4. package scanner
  5. import (
  6. "bytes"
  7. "errors"
  8. "fmt"
  9. "io/ioutil"
  10. "os"
  11. "path/filepath"
  12. "runtime"
  13. "strings"
  14. "time"
  15. "code.google.com/p/go.text/unicode/norm"
  16. "github.com/calmh/syncthing/lamport"
  17. "github.com/calmh/syncthing/protocol"
  18. )
  19. type Walker struct {
  20. // Dir is the base directory for the walk
  21. Dir string
  22. // BlockSize controls the size of the block used when hashing.
  23. BlockSize int
  24. // If IgnoreFile is not empty, it is the name used for the file that holds ignore patterns.
  25. IgnoreFile string
  26. // If TempNamer is not nil, it is used to ignore tempory files when walking.
  27. TempNamer TempNamer
  28. // If CurrentFiler is not nil, it is queried for the current file before rescanning.
  29. CurrentFiler CurrentFiler
  30. // If Suppressor is not nil, it is queried for supression of modified files.
  31. // Suppressed files will be returned with empty metadata and the Suppressed flag set.
  32. // Requires CurrentFiler to be set.
  33. Suppressor Suppressor
  34. // If IgnorePerms is true, changes to permission bits will not be
  35. // detected. Scanned files will get zero permission bits and the
  36. // NoPermissionBits flag set.
  37. IgnorePerms bool
  38. }
  39. type TempNamer interface {
  40. // Temporary returns a temporary name for the filed referred to by filepath.
  41. TempName(path string) string
  42. // IsTemporary returns true if path refers to the name of temporary file.
  43. IsTemporary(path string) bool
  44. }
  45. type Suppressor interface {
  46. // Supress returns true if the update to the named file should be ignored.
  47. Suppress(name string, fi os.FileInfo) (bool, bool)
  48. }
  49. type CurrentFiler interface {
  50. // CurrentFile returns the file as seen at last scan.
  51. CurrentFile(name string) protocol.FileInfo
  52. }
  53. // Walk returns the list of files found in the local repository by scanning the
  54. // file system. Files are blockwise hashed.
  55. func (w *Walker) Walk() (files chan protocol.FileInfo, ignore map[string][]string, err error) {
  56. if debug {
  57. l.Debugln("Walk", w.Dir, w.BlockSize, w.IgnoreFile)
  58. }
  59. err = checkDir(w.Dir)
  60. if err != nil {
  61. return
  62. }
  63. ignore = make(map[string][]string)
  64. files = make(chan protocol.FileInfo)
  65. hashFiles := w.walkAndHashFiles(files, ignore)
  66. go func() {
  67. filepath.Walk(w.Dir, w.loadIgnoreFiles(w.Dir, ignore))
  68. filepath.Walk(w.Dir, hashFiles)
  69. close(files)
  70. }()
  71. return
  72. }
  73. // CleanTempFiles removes all files that match the temporary filename pattern.
  74. func (w *Walker) CleanTempFiles() {
  75. filepath.Walk(w.Dir, w.cleanTempFile)
  76. }
  77. func (w *Walker) loadIgnoreFiles(dir string, ign map[string][]string) filepath.WalkFunc {
  78. return func(p string, info os.FileInfo, err error) error {
  79. if err != nil {
  80. return nil
  81. }
  82. rn, err := filepath.Rel(dir, p)
  83. if err != nil {
  84. return nil
  85. }
  86. if pn, sn := filepath.Split(rn); sn == w.IgnoreFile {
  87. pn := filepath.Clean(pn)
  88. bs, _ := ioutil.ReadFile(p)
  89. lines := bytes.Split(bs, []byte("\n"))
  90. var patterns []string
  91. for _, line := range lines {
  92. lineStr := strings.TrimSpace(string(line))
  93. if len(lineStr) > 0 {
  94. patterns = append(patterns, lineStr)
  95. }
  96. }
  97. ign[pn] = patterns
  98. }
  99. return nil
  100. }
  101. }
  102. func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo, ign map[string][]string) filepath.WalkFunc {
  103. return func(p string, info os.FileInfo, err error) error {
  104. if err != nil {
  105. if debug {
  106. l.Debugln("error:", p, info, err)
  107. }
  108. return nil
  109. }
  110. rn, err := filepath.Rel(w.Dir, p)
  111. if err != nil {
  112. if debug {
  113. l.Debugln("rel error:", p, err)
  114. }
  115. return nil
  116. }
  117. if rn == "." {
  118. return nil
  119. }
  120. if w.TempNamer != nil && w.TempNamer.IsTemporary(rn) {
  121. // A temporary file
  122. if debug {
  123. l.Debugln("temporary:", rn)
  124. }
  125. return nil
  126. }
  127. if sn := filepath.Base(rn); sn == w.IgnoreFile || sn == ".stversions" || w.ignoreFile(ign, rn) {
  128. // An ignored file
  129. if debug {
  130. l.Debugln("ignored:", rn)
  131. }
  132. if info.IsDir() {
  133. return filepath.SkipDir
  134. }
  135. return nil
  136. }
  137. if (runtime.GOOS == "linux" || runtime.GOOS == "windows") && !norm.NFC.IsNormalString(rn) {
  138. l.Warnf("File %q contains non-NFC UTF-8 sequences and cannot be synced. Consider renaming.", rn)
  139. return nil
  140. }
  141. if info.Mode().IsDir() {
  142. if w.CurrentFiler != nil {
  143. cf := w.CurrentFiler.CurrentFile(rn)
  144. permUnchanged := w.IgnorePerms || !protocol.HasPermissionBits(cf.Flags) || PermsEqual(cf.Flags, uint32(info.Mode()))
  145. if !protocol.IsDeleted(cf.Flags) && protocol.IsDirectory(cf.Flags) && permUnchanged {
  146. return nil
  147. }
  148. }
  149. var flags uint32 = protocol.FlagDirectory
  150. if w.IgnorePerms {
  151. flags |= protocol.FlagNoPermBits | 0777
  152. } else {
  153. flags |= uint32(info.Mode() & os.ModePerm)
  154. }
  155. f := protocol.FileInfo{
  156. Name: rn,
  157. Version: lamport.Default.Tick(0),
  158. Flags: flags,
  159. Modified: info.ModTime().Unix(),
  160. }
  161. if debug {
  162. l.Debugln("dir:", f)
  163. }
  164. fchan <- f
  165. return nil
  166. }
  167. if info.Mode().IsRegular() {
  168. if w.CurrentFiler != nil {
  169. cf := w.CurrentFiler.CurrentFile(rn)
  170. permUnchanged := w.IgnorePerms || !protocol.HasPermissionBits(cf.Flags) || PermsEqual(cf.Flags, uint32(info.Mode()))
  171. if !protocol.IsDeleted(cf.Flags) && cf.Modified == info.ModTime().Unix() && permUnchanged {
  172. return nil
  173. }
  174. if w.Suppressor != nil {
  175. if cur, prev := w.Suppressor.Suppress(rn, info); cur && !prev {
  176. l.Infof("Changes to %q are being temporarily suppressed because it changes too frequently.", p)
  177. cf.Flags |= protocol.FlagInvalid
  178. cf.Version = lamport.Default.Tick(cf.Version)
  179. cf.LocalVersion = 0
  180. if debug {
  181. l.Debugln("suppressed:", cf)
  182. }
  183. fchan <- cf
  184. return nil
  185. } else if prev && !cur {
  186. l.Infof("Changes to %q are no longer suppressed.", p)
  187. }
  188. }
  189. if debug {
  190. l.Debugln("rescan:", cf, info.ModTime().Unix(), info.Mode()&os.ModePerm)
  191. }
  192. }
  193. fd, err := os.Open(p)
  194. if err != nil {
  195. if debug {
  196. l.Debugln("open:", p, err)
  197. }
  198. return nil
  199. }
  200. defer fd.Close()
  201. t0 := time.Now()
  202. blocks, err := Blocks(fd, w.BlockSize)
  203. if err != nil {
  204. if debug {
  205. l.Debugln("hash error:", rn, err)
  206. }
  207. return nil
  208. }
  209. if debug {
  210. t1 := time.Now()
  211. l.Debugln("hashed:", rn, ";", len(blocks), "blocks;", info.Size(), "bytes;", int(float64(info.Size())/1024/t1.Sub(t0).Seconds()), "KB/s")
  212. }
  213. var flags = uint32(info.Mode() & os.ModePerm)
  214. if w.IgnorePerms {
  215. flags = protocol.FlagNoPermBits | 0666
  216. }
  217. f := protocol.FileInfo{
  218. Name: rn,
  219. Version: lamport.Default.Tick(0),
  220. Flags: flags,
  221. Modified: info.ModTime().Unix(),
  222. Blocks: blocks,
  223. }
  224. fchan <- f
  225. }
  226. return nil
  227. }
  228. }
  229. func (w *Walker) cleanTempFile(path string, info os.FileInfo, err error) error {
  230. if err != nil {
  231. return err
  232. }
  233. if info.Mode()&os.ModeType == 0 && w.TempNamer.IsTemporary(path) {
  234. os.Remove(path)
  235. }
  236. return nil
  237. }
  238. func (w *Walker) ignoreFile(patterns map[string][]string, file string) bool {
  239. first, last := filepath.Split(file)
  240. for prefix, pats := range patterns {
  241. if prefix == "." || prefix == first || strings.HasPrefix(first, fmt.Sprintf("%s%c", prefix, os.PathSeparator)) {
  242. for _, pattern := range pats {
  243. if match, _ := filepath.Match(pattern, last); match {
  244. return true
  245. }
  246. }
  247. }
  248. }
  249. return false
  250. }
  251. func checkDir(dir string) error {
  252. if info, err := os.Lstat(dir); err != nil {
  253. return err
  254. } else if !info.IsDir() {
  255. return errors.New(dir + ": not a directory")
  256. } else if debug {
  257. l.Debugln("checkDir", dir, info)
  258. }
  259. return nil
  260. }
  261. func PermsEqual(a, b uint32) bool {
  262. switch runtime.GOOS {
  263. case "windows":
  264. // There is only writeable and read only, represented for user, group
  265. // and other equally. We only compare against user.
  266. return a&0600 == b&0600
  267. default:
  268. // All bits count
  269. return a&0777 == b&0777
  270. }
  271. }