syncthing/lib/ignore/ignore.go

292 lines
6.1 KiB
Go
Raw Normal View History

2014-11-16 20:13:20 +00:00
// Copyright (C) 2014 The Syncthing Authors.
2014-09-29 19:43:32 +00:00
//
2015-03-07 20:36:35 +00:00
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package ignore
import (
"bufio"
"bytes"
"crypto/md5"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"time"
2015-08-06 09:29:25 +00:00
"github.com/syncthing/syncthing/lib/fnmatch"
"github.com/syncthing/syncthing/lib/sync"
)
type Pattern struct {
match *regexp.Regexp
include bool
}
func (p Pattern) String() string {
if p.include {
return p.match.String()
}
2015-04-28 20:32:10 +00:00
return "(?exclude)" + p.match.String()
}
2014-10-12 21:35:15 +00:00
type Matcher struct {
patterns []Pattern
withCache bool
matches *cache
curHash string
stop chan struct{}
mut sync.Mutex
}
func New(withCache bool) *Matcher {
m := &Matcher{
withCache: withCache,
stop: make(chan struct{}),
2015-04-22 22:54:31 +00:00
mut: sync.NewMutex(),
}
if withCache {
go m.clean(2 * time.Hour)
}
return m
2014-10-12 21:35:15 +00:00
}
func (m *Matcher) Load(file string) error {
// No locking, Parse() does the locking
fd, err := os.Open(file)
if err != nil {
// We do a parse with empty patterns to clear out the hash, cache etc.
m.Parse(&bytes.Buffer{}, file)
return err
}
defer fd.Close()
return m.Parse(fd, file)
}
func (m *Matcher) Parse(r io.Reader, file string) error {
m.mut.Lock()
defer m.mut.Unlock()
seen := map[string]bool{file: true}
patterns, err := parseIgnoreFile(r, file, seen)
// Error is saved and returned at the end. We process the patterns
// (possibly blank) anyway.
newHash := hashPatterns(patterns)
if newHash == m.curHash {
// We've already loaded exactly these patterns.
return err
}
m.curHash = newHash
m.patterns = patterns
if m.withCache {
m.matches = newCache(patterns)
}
return err
}
2014-10-12 21:35:15 +00:00
func (m *Matcher) Match(file string) (result bool) {
2015-04-27 19:49:10 +00:00
if m == nil {
return false
}
m.mut.Lock()
defer m.mut.Unlock()
2014-10-12 21:35:15 +00:00
if len(m.patterns) == 0 {
return false
}
if m.matches != nil {
// Check the cache for a known result.
res, ok := m.matches.get(file)
2014-10-12 21:35:15 +00:00
if ok {
return res
2014-10-12 21:35:15 +00:00
}
// Update the cache with the result at return time
defer func() {
m.matches.set(file, result)
}()
2014-10-12 21:35:15 +00:00
}
// Check all the patterns for a match.
2014-10-12 21:35:15 +00:00
for _, pattern := range m.patterns {
if pattern.match.MatchString(file) {
return pattern.include
}
}
// Default to false.
return false
}
// Patterns return a list of the loaded regexp patterns, as strings
func (m *Matcher) Patterns() []string {
2015-04-27 19:49:10 +00:00
if m == nil {
return nil
}
m.mut.Lock()
defer m.mut.Unlock()
patterns := make([]string, len(m.patterns))
for i, pat := range m.patterns {
patterns[i] = pat.String()
}
return patterns
}
func (m *Matcher) Hash() string {
m.mut.Lock()
defer m.mut.Unlock()
return m.curHash
}
func (m *Matcher) Stop() {
close(m.stop)
}
func (m *Matcher) clean(d time.Duration) {
t := time.NewTimer(d / 2)
for {
select {
case <-m.stop:
return
case <-t.C:
m.mut.Lock()
if m.matches != nil {
m.matches.clean(d)
}
t.Reset(d / 2)
m.mut.Unlock()
}
}
}
func hashPatterns(patterns []Pattern) string {
h := md5.New()
for _, pat := range patterns {
h.Write([]byte(pat.String()))
h.Write([]byte("\n"))
}
return fmt.Sprintf("%x", h.Sum(nil))
}
func loadIgnoreFile(file string, seen map[string]bool) ([]Pattern, error) {
if seen[file] {
return nil, fmt.Errorf("Multiple include of ignore file %q", file)
}
seen[file] = true
fd, err := os.Open(file)
if err != nil {
return nil, err
}
defer fd.Close()
return parseIgnoreFile(fd, file, seen)
}
func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]Pattern, error) {
var patterns []Pattern
addPattern := func(line string) error {
include := true
if strings.HasPrefix(line, "!") {
line = line[1:]
include = false
}
flags := fnmatch.PathName
if strings.HasPrefix(line, "(?i)") {
line = line[4:]
flags |= fnmatch.CaseFold
}
if strings.HasPrefix(line, "/") {
// Pattern is rooted in the current dir only
exp, err := fnmatch.Convert(line[1:], flags)
if err != nil {
return fmt.Errorf("invalid pattern %q in ignore file", line)
}
patterns = append(patterns, Pattern{exp, include})
} else if strings.HasPrefix(line, "**/") {
// Add the pattern as is, and without **/ so it matches in current dir
exp, err := fnmatch.Convert(line, flags)
if err != nil {
return fmt.Errorf("invalid pattern %q in ignore file", line)
}
patterns = append(patterns, Pattern{exp, include})
exp, err = fnmatch.Convert(line[3:], flags)
if err != nil {
return fmt.Errorf("invalid pattern %q in ignore file", line)
}
patterns = append(patterns, Pattern{exp, include})
} else if strings.HasPrefix(line, "#include ") {
includeRel := line[len("#include "):]
includeFile := filepath.Join(filepath.Dir(currentFile), includeRel)
includes, err := loadIgnoreFile(includeFile, seen)
if err != nil {
return fmt.Errorf("include of %q: %v", includeRel, err)
}
patterns = append(patterns, includes...)
} else {
// Path name or pattern, add it so it matches files both in
// current directory and subdirs.
exp, err := fnmatch.Convert(line, flags)
if err != nil {
return fmt.Errorf("invalid pattern %q in ignore file", line)
}
patterns = append(patterns, Pattern{exp, include})
exp, err = fnmatch.Convert("**/"+line, flags)
if err != nil {
return fmt.Errorf("invalid pattern %q in ignore file", line)
}
patterns = append(patterns, Pattern{exp, include})
}
return nil
}
scanner := bufio.NewScanner(fd)
var err error
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
switch {
case line == "":
continue
2014-09-16 21:22:21 +00:00
case strings.HasPrefix(line, "//"):
continue
case strings.HasPrefix(line, "#"):
err = addPattern(line)
case strings.HasSuffix(line, "/**"):
err = addPattern(line)
case strings.HasSuffix(line, "/"):
err = addPattern(line)
if err == nil {
err = addPattern(line + "**")
}
default:
err = addPattern(line)
if err == nil {
err = addPattern(line + "/**")
}
}
if err != nil {
return nil, err
}
}
return patterns, nil
}