ignore.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. // Copyright (C) 2014 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 ignore
  7. import (
  8. "bufio"
  9. "bytes"
  10. "crypto/md5"
  11. "fmt"
  12. "io"
  13. "path/filepath"
  14. "runtime"
  15. "strings"
  16. "time"
  17. "github.com/gobwas/glob"
  18. "github.com/pkg/errors"
  19. "github.com/syncthing/syncthing/lib/fs"
  20. "github.com/syncthing/syncthing/lib/osutil"
  21. "github.com/syncthing/syncthing/lib/sync"
  22. )
  23. const (
  24. resultNotMatched Result = 0
  25. resultInclude Result = 1 << iota
  26. resultDeletable = 1 << iota
  27. resultFoldCase = 1 << iota
  28. )
  29. var defaultResult Result = resultInclude
  30. func init() {
  31. if runtime.GOOS == "darwin" || runtime.GOOS == "windows" {
  32. defaultResult |= resultFoldCase
  33. }
  34. }
  35. type Pattern struct {
  36. pattern string
  37. match glob.Glob
  38. result Result
  39. }
  40. func (p Pattern) String() string {
  41. ret := p.pattern
  42. if p.result&resultInclude != resultInclude {
  43. ret = "!" + ret
  44. }
  45. if p.result&resultFoldCase == resultFoldCase {
  46. ret = "(?i)" + ret
  47. }
  48. if p.result&resultDeletable == resultDeletable {
  49. ret = "(?d)" + ret
  50. }
  51. return ret
  52. }
  53. func (p Pattern) allowsSkippingIgnoredDirs() bool {
  54. if p.result.IsIgnored() {
  55. return true
  56. }
  57. if p.pattern[0] != '/' {
  58. return false
  59. }
  60. if strings.Contains(p.pattern[1:], "/") {
  61. return false
  62. }
  63. // Double asterisk everywhere in the path except at the end is bad
  64. return !strings.Contains(strings.TrimSuffix(p.pattern, "**"), "**")
  65. }
  66. type Result uint8
  67. func (r Result) IsIgnored() bool {
  68. return r&resultInclude == resultInclude
  69. }
  70. func (r Result) IsDeletable() bool {
  71. return r.IsIgnored() && r&resultDeletable == resultDeletable
  72. }
  73. func (r Result) IsCaseFolded() bool {
  74. return r&resultFoldCase == resultFoldCase
  75. }
  76. // The ChangeDetector is responsible for determining if files have changed
  77. // on disk. It gets told to Remember() files (name and modtime) and will
  78. // then get asked if a file has been Seen() (i.e., Remember() has been
  79. // called on it) and if any of the files have Changed(). To forget all
  80. // files, call Reset().
  81. type ChangeDetector interface {
  82. Remember(fs fs.Filesystem, name string, modtime time.Time)
  83. Seen(fs fs.Filesystem, name string) bool
  84. Changed() bool
  85. Reset()
  86. }
  87. type Matcher struct {
  88. fs fs.Filesystem
  89. lines []string // exact lines read from .stignore
  90. patterns []Pattern // patterns including those from included files
  91. withCache bool
  92. matches *cache
  93. curHash string
  94. stop chan struct{}
  95. changeDetector ChangeDetector
  96. skipIgnoredDirs bool
  97. mut sync.Mutex
  98. }
  99. // An Option can be passed to New()
  100. type Option func(*Matcher)
  101. // WithCache enables or disables lookup caching. The default is disabled.
  102. func WithCache(v bool) Option {
  103. return func(m *Matcher) {
  104. m.withCache = v
  105. }
  106. }
  107. // WithChangeDetector sets a custom ChangeDetector. The default is to simply
  108. // use the on disk modtime for comparison.
  109. func WithChangeDetector(cd ChangeDetector) Option {
  110. return func(m *Matcher) {
  111. m.changeDetector = cd
  112. }
  113. }
  114. func New(fs fs.Filesystem, opts ...Option) *Matcher {
  115. m := &Matcher{
  116. fs: fs,
  117. stop: make(chan struct{}),
  118. mut: sync.NewMutex(),
  119. skipIgnoredDirs: true,
  120. }
  121. for _, opt := range opts {
  122. opt(m)
  123. }
  124. if m.changeDetector == nil {
  125. m.changeDetector = newModtimeChecker()
  126. }
  127. if m.withCache {
  128. go m.clean(2 * time.Hour)
  129. }
  130. return m
  131. }
  132. func (m *Matcher) Load(file string) error {
  133. m.mut.Lock()
  134. defer m.mut.Unlock()
  135. if m.changeDetector.Seen(m.fs, file) && !m.changeDetector.Changed() {
  136. return nil
  137. }
  138. fd, info, err := loadIgnoreFile(m.fs, file, m.changeDetector)
  139. if err != nil {
  140. m.parseLocked(&bytes.Buffer{}, file)
  141. return err
  142. }
  143. defer fd.Close()
  144. m.changeDetector.Reset()
  145. err = m.parseLocked(fd, file)
  146. // If we failed to parse, don't cache, as next time Load is called
  147. // we'll pretend it's all good.
  148. if err == nil {
  149. m.changeDetector.Remember(m.fs, file, info.ModTime())
  150. }
  151. return err
  152. }
  153. func (m *Matcher) Parse(r io.Reader, file string) error {
  154. m.mut.Lock()
  155. defer m.mut.Unlock()
  156. return m.parseLocked(r, file)
  157. }
  158. func (m *Matcher) parseLocked(r io.Reader, file string) error {
  159. lines, patterns, err := parseIgnoreFile(m.fs, r, file, m.changeDetector, make(map[string]struct{}))
  160. // Error is saved and returned at the end. We process the patterns
  161. // (possibly blank) anyway.
  162. m.lines = lines
  163. newHash := hashPatterns(patterns)
  164. if newHash == m.curHash {
  165. // We've already loaded exactly these patterns.
  166. return err
  167. }
  168. m.skipIgnoredDirs = true
  169. var previous string
  170. for _, p := range patterns {
  171. // We automatically add patterns with a /** suffix, which normally
  172. // means that we cannot skip directories. However if the same
  173. // pattern without the /** already exists (which is true for
  174. // automatically added patterns) we can skip.
  175. if l := len(p.pattern); l > 3 && p.pattern[:len(p.pattern)-3] == previous {
  176. continue
  177. }
  178. if !p.allowsSkippingIgnoredDirs() {
  179. m.skipIgnoredDirs = false
  180. break
  181. }
  182. previous = p.pattern
  183. }
  184. m.curHash = newHash
  185. m.patterns = patterns
  186. if m.withCache {
  187. m.matches = newCache(patterns)
  188. }
  189. return err
  190. }
  191. func (m *Matcher) Match(file string) (result Result) {
  192. if file == "." {
  193. return resultNotMatched
  194. }
  195. m.mut.Lock()
  196. defer m.mut.Unlock()
  197. if len(m.patterns) == 0 {
  198. return resultNotMatched
  199. }
  200. if m.matches != nil {
  201. // Check the cache for a known result.
  202. res, ok := m.matches.get(file)
  203. if ok {
  204. return res
  205. }
  206. // Update the cache with the result at return time
  207. defer func() {
  208. m.matches.set(file, result)
  209. }()
  210. }
  211. // Check all the patterns for a match.
  212. file = filepath.ToSlash(file)
  213. var lowercaseFile string
  214. for _, pattern := range m.patterns {
  215. if pattern.result.IsCaseFolded() {
  216. if lowercaseFile == "" {
  217. lowercaseFile = strings.ToLower(file)
  218. }
  219. if pattern.match.Match(lowercaseFile) {
  220. return pattern.result
  221. }
  222. } else {
  223. if pattern.match.Match(file) {
  224. return pattern.result
  225. }
  226. }
  227. }
  228. // Default to not matching.
  229. return resultNotMatched
  230. }
  231. // Lines return a list of the unprocessed lines in .stignore at last load
  232. func (m *Matcher) Lines() []string {
  233. m.mut.Lock()
  234. defer m.mut.Unlock()
  235. return m.lines
  236. }
  237. // Patterns return a list of the loaded patterns, as they've been parsed
  238. func (m *Matcher) Patterns() []string {
  239. m.mut.Lock()
  240. defer m.mut.Unlock()
  241. patterns := make([]string, len(m.patterns))
  242. for i, pat := range m.patterns {
  243. patterns[i] = pat.String()
  244. }
  245. return patterns
  246. }
  247. func (m *Matcher) String() string {
  248. return fmt.Sprintf("Matcher/%v@%p", m.Patterns(), m)
  249. }
  250. func (m *Matcher) Hash() string {
  251. m.mut.Lock()
  252. defer m.mut.Unlock()
  253. return m.curHash
  254. }
  255. func (m *Matcher) Stop() {
  256. close(m.stop)
  257. }
  258. func (m *Matcher) clean(d time.Duration) {
  259. t := time.NewTimer(d / 2)
  260. for {
  261. select {
  262. case <-m.stop:
  263. return
  264. case <-t.C:
  265. m.mut.Lock()
  266. if m.matches != nil {
  267. m.matches.clean(d)
  268. }
  269. t.Reset(d / 2)
  270. m.mut.Unlock()
  271. }
  272. }
  273. }
  274. // ShouldIgnore returns true when a file is temporary, internal or ignored
  275. func (m *Matcher) ShouldIgnore(filename string) bool {
  276. switch {
  277. case fs.IsTemporary(filename):
  278. return true
  279. case fs.IsInternal(filename):
  280. return true
  281. case m.Match(filename).IsIgnored():
  282. return true
  283. }
  284. return false
  285. }
  286. func (m *Matcher) SkipIgnoredDirs() bool {
  287. m.mut.Lock()
  288. defer m.mut.Unlock()
  289. return m.skipIgnoredDirs
  290. }
  291. func hashPatterns(patterns []Pattern) string {
  292. h := md5.New()
  293. for _, pat := range patterns {
  294. h.Write([]byte(pat.String()))
  295. h.Write([]byte("\n"))
  296. }
  297. return fmt.Sprintf("%x", h.Sum(nil))
  298. }
  299. func loadIgnoreFile(fs fs.Filesystem, file string, cd ChangeDetector) (fs.File, fs.FileInfo, error) {
  300. fd, err := fs.Open(file)
  301. if err != nil {
  302. return fd, nil, err
  303. }
  304. info, err := fd.Stat()
  305. if err != nil {
  306. fd.Close()
  307. }
  308. return fd, info, err
  309. }
  310. func loadParseIncludeFile(filesystem fs.Filesystem, file string, cd ChangeDetector, linesSeen map[string]struct{}) ([]Pattern, error) {
  311. // Allow escaping the folders filesystem.
  312. // TODO: Deprecate, somehow?
  313. if filesystem.Type() == fs.FilesystemTypeBasic {
  314. uri := filesystem.URI()
  315. joined := filepath.Join(uri, file)
  316. if !fs.IsParent(joined, uri) {
  317. filesystem = fs.NewFilesystem(filesystem.Type(), filepath.Dir(joined))
  318. file = filepath.Base(joined)
  319. }
  320. }
  321. if cd.Seen(filesystem, file) {
  322. return nil, fmt.Errorf("multiple include of ignore file %q", file)
  323. }
  324. fd, info, err := loadIgnoreFile(filesystem, file, cd)
  325. if err != nil {
  326. return nil, err
  327. }
  328. defer fd.Close()
  329. cd.Remember(filesystem, file, info.ModTime())
  330. _, patterns, err := parseIgnoreFile(filesystem, fd, file, cd, linesSeen)
  331. return patterns, err
  332. }
  333. func parseLine(line string) ([]Pattern, error) {
  334. pattern := Pattern{
  335. result: defaultResult,
  336. }
  337. // Allow prefixes to be specified in any order, but only once.
  338. var seenPrefix [3]bool
  339. for {
  340. if strings.HasPrefix(line, "!") && !seenPrefix[0] {
  341. seenPrefix[0] = true
  342. line = line[1:]
  343. pattern.result ^= resultInclude
  344. } else if strings.HasPrefix(line, "(?i)") && !seenPrefix[1] {
  345. seenPrefix[1] = true
  346. pattern.result |= resultFoldCase
  347. line = line[4:]
  348. } else if strings.HasPrefix(line, "(?d)") && !seenPrefix[2] {
  349. seenPrefix[2] = true
  350. pattern.result |= resultDeletable
  351. line = line[4:]
  352. } else {
  353. break
  354. }
  355. }
  356. if line == "" {
  357. return nil, errors.New("missing pattern")
  358. }
  359. if pattern.result.IsCaseFolded() {
  360. line = strings.ToLower(line)
  361. }
  362. pattern.pattern = line
  363. var err error
  364. if strings.HasPrefix(line, "/") {
  365. // Pattern is rooted in the current dir only
  366. pattern.match, err = glob.Compile(line[1:], '/')
  367. return []Pattern{pattern}, err
  368. }
  369. patterns := make([]Pattern, 2)
  370. if strings.HasPrefix(line, "**/") {
  371. // Add the pattern as is, and without **/ so it matches in current dir
  372. pattern.match, err = glob.Compile(line, '/')
  373. if err != nil {
  374. return nil, err
  375. }
  376. patterns[0] = pattern
  377. line = line[3:]
  378. pattern.pattern = line
  379. pattern.match, err = glob.Compile(line, '/')
  380. if err != nil {
  381. return nil, err
  382. }
  383. patterns[1] = pattern
  384. return patterns, nil
  385. }
  386. // Path name or pattern, add it so it matches files both in
  387. // current directory and subdirs.
  388. pattern.match, err = glob.Compile(line, '/')
  389. if err != nil {
  390. return nil, err
  391. }
  392. patterns[0] = pattern
  393. line = "**/" + line
  394. pattern.pattern = line
  395. pattern.match, err = glob.Compile(line, '/')
  396. if err != nil {
  397. return nil, err
  398. }
  399. patterns[1] = pattern
  400. return patterns, nil
  401. }
  402. func parseIgnoreFile(fs fs.Filesystem, fd io.Reader, currentFile string, cd ChangeDetector, linesSeen map[string]struct{}) ([]string, []Pattern, error) {
  403. var lines []string
  404. var patterns []Pattern
  405. addPattern := func(line string) error {
  406. newPatterns, err := parseLine(line)
  407. if err != nil {
  408. return errors.Wrapf(err, "invalid pattern %q in ignore file", line)
  409. }
  410. patterns = append(patterns, newPatterns...)
  411. return nil
  412. }
  413. scanner := bufio.NewScanner(fd)
  414. var err error
  415. for scanner.Scan() {
  416. line := strings.TrimSpace(scanner.Text())
  417. lines = append(lines, line)
  418. if _, ok := linesSeen[line]; ok {
  419. continue
  420. }
  421. linesSeen[line] = struct{}{}
  422. switch {
  423. case line == "":
  424. continue
  425. case strings.HasPrefix(line, "//"):
  426. continue
  427. }
  428. line = filepath.ToSlash(line)
  429. switch {
  430. case strings.HasPrefix(line, "#include"):
  431. fields := strings.SplitN(line, " ", 2)
  432. if len(fields) != 2 {
  433. err = errors.New("failed to parse #include line: no file?")
  434. break
  435. }
  436. includeRel := strings.TrimSpace(fields[1])
  437. if includeRel == "" {
  438. err = errors.New("failed to parse #include line: no file?")
  439. break
  440. }
  441. includeFile := filepath.Join(filepath.Dir(currentFile), includeRel)
  442. var includePatterns []Pattern
  443. if includePatterns, err = loadParseIncludeFile(fs, includeFile, cd, linesSeen); err == nil {
  444. patterns = append(patterns, includePatterns...)
  445. } else {
  446. // Wrap the error, as if the include does not exist, we get a
  447. // IsNotExists(err) == true error, which we use to check
  448. // existance of the .stignore file, and just end up assuming
  449. // there is none, rather than a broken include.
  450. err = fmt.Errorf("failed to load include file %s: %s", includeFile, err.Error())
  451. }
  452. case strings.HasSuffix(line, "/**"):
  453. err = addPattern(line)
  454. case strings.HasSuffix(line, "/"):
  455. err = addPattern(line + "**")
  456. default:
  457. err = addPattern(line)
  458. if err == nil {
  459. err = addPattern(line + "/**")
  460. }
  461. }
  462. if err != nil {
  463. return nil, nil, err
  464. }
  465. }
  466. return lines, patterns, nil
  467. }
  468. // WriteIgnores is a convenience function to avoid code duplication
  469. func WriteIgnores(filesystem fs.Filesystem, path string, content []string) error {
  470. if len(content) == 0 {
  471. err := filesystem.Remove(path)
  472. if fs.IsNotExist(err) {
  473. return nil
  474. }
  475. return err
  476. }
  477. fd, err := osutil.CreateAtomicFilesystem(filesystem, path)
  478. if err != nil {
  479. return err
  480. }
  481. for _, line := range content {
  482. fmt.Fprintln(fd, line)
  483. }
  484. if err := fd.Close(); err != nil {
  485. return err
  486. }
  487. filesystem.Hide(path)
  488. return nil
  489. }
  490. type modtimeCheckerKey struct {
  491. fs fs.Filesystem
  492. name string
  493. }
  494. // modtimeChecker is the default implementation of ChangeDetector
  495. type modtimeChecker struct {
  496. modtimes map[modtimeCheckerKey]time.Time
  497. }
  498. func newModtimeChecker() *modtimeChecker {
  499. return &modtimeChecker{
  500. modtimes: map[modtimeCheckerKey]time.Time{},
  501. }
  502. }
  503. func (c *modtimeChecker) Remember(fs fs.Filesystem, name string, modtime time.Time) {
  504. c.modtimes[modtimeCheckerKey{fs, name}] = modtime
  505. }
  506. func (c *modtimeChecker) Seen(fs fs.Filesystem, name string) bool {
  507. _, ok := c.modtimes[modtimeCheckerKey{fs, name}]
  508. return ok
  509. }
  510. func (c *modtimeChecker) Reset() {
  511. c.modtimes = map[modtimeCheckerKey]time.Time{}
  512. }
  513. func (c *modtimeChecker) Changed() bool {
  514. for key, modtime := range c.modtimes {
  515. info, err := key.fs.Stat(key.name)
  516. if err != nil {
  517. return true
  518. }
  519. if !info.ModTime().Equal(modtime) {
  520. return true
  521. }
  522. }
  523. return false
  524. }