aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob Borg <jakob@nym.se>2014-09-04 22:29:53 +0200
committerJakob Borg <jakob@nym.se>2014-09-04 22:30:42 +0200
commit92c44c8abecba900969c38ab17cbd786268828a6 (patch)
treed4253edc6b442ad25826196034e9925f28982c71
parent8e4f7bbd3e624089256257b1d1ed0140e03bf245 (diff)
downloadsyncthing-92c44c8abecba900969c38ab17cbd786268828a6.tar.gz
syncthing-92c44c8abecba900969c38ab17cbd786268828a6.zip
Rework .stignore functionality (fixes #561) (...)
- Only one .stignore is supported, at the repo root - Negative patterns (!) are supported - Ignore patterns affect sent and received indexes, not only scanning
-rw-r--r--auto/gui.files.go2
-rw-r--r--cmd/syncthing/gui.go2
-rw-r--r--files/leveldb.go38
-rw-r--r--files/set_test.go82
-rw-r--r--gui/app.js2
-rw-r--r--ignore/ignore.go146
-rw-r--r--ignore/ignore_test.go104
-rw-r--r--ignore/testdata/.stignore6
-rw-r--r--ignore/testdata/dir3/cfile1
-rw-r--r--ignore/testdata/dir3/dfile1
-rw-r--r--ignore/testdata/excludes2
-rw-r--r--ignore/testdata/further-excludes1
-rwxr-xr-xintegration/all.sh2
-rw-r--r--model/model.go106
-rw-r--r--scanner/testdata/.stignore1
-rw-r--r--scanner/testdata/excludes2
-rw-r--r--scanner/testdata/loop-excludes1
-rw-r--r--scanner/walk.go140
-rw-r--r--scanner/walk_test.go100
19 files changed, 488 insertions, 251 deletions
diff --git a/auto/gui.files.go b/auto/gui.files.go
index 3a294a082..65c44f4a1 100644
--- a/auto/gui.files.go
+++ b/auto/gui.files.go
@@ -27,7 +27,7 @@ func Assets() map[string][]byte {
bs, _ = ioutil.ReadAll(gr)
assets["angular/angular.min.js"] = bs
- bs, _ = base64.StdEncoding.DecodeString("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")
+ bs, _ = base64.StdEncoding.DecodeString("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")
gr, _ = gzip.NewReader(bytes.NewBuffer(bs))
bs, _ = ioutil.ReadAll(gr)
assets["app.js"] = bs
diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go
index 8624d120c..682d37002 100644
--- a/cmd/syncthing/gui.go
+++ b/cmd/syncthing/gui.go
@@ -255,7 +255,7 @@ func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) {
func restPostOverride(m *model.Model, w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
var repo = qs.Get("repo")
- m.Override(repo)
+ go m.Override(repo)
}
func restGetNeed(m *model.Model, w http.ResponseWriter, r *http.Request) {
diff --git a/files/leveldb.go b/files/leveldb.go
index 7691b75da..7f970eb7f 100644
--- a/files/leveldb.go
+++ b/files/leveldb.go
@@ -186,18 +186,28 @@ func ldbGenericReplace(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo
if lv := ldbInsert(batch, repo, node, newName, fs[fsi]); lv > maxLocalVer {
maxLocalVer = lv
}
- ldbUpdateGlobal(snap, batch, repo, node, newName, fs[fsi].Version)
+ if fs[fsi].IsInvalid() {
+ ldbRemoveFromGlobal(snap, batch, repo, node, newName)
+ } else {
+ ldbUpdateGlobal(snap, batch, repo, node, newName, fs[fsi].Version)
+ }
fsi++
case moreFs && moreDb && cmp == 0:
- // File exists on both sides - compare versions.
+ // File exists on both sides - compare versions. We might get an
+ // update with the same version and different flags if a node has
+ // marked a file as invalid, so handle that too.
var ef protocol.FileInfoTruncated
ef.UnmarshalXDR(dbi.Value())
- if fs[fsi].Version > ef.Version {
+ if fs[fsi].Version > ef.Version || fs[fsi].Version != ef.Version {
if lv := ldbInsert(batch, repo, node, newName, fs[fsi]); lv > maxLocalVer {
maxLocalVer = lv
}
- ldbUpdateGlobal(snap, batch, repo, node, newName, fs[fsi].Version)
+ if fs[fsi].IsInvalid() {
+ ldbRemoveFromGlobal(snap, batch, repo, node, newName)
+ } else {
+ ldbUpdateGlobal(snap, batch, repo, node, newName, fs[fsi].Version)
+ }
}
// Iterate both sides.
fsi++
@@ -280,7 +290,11 @@ func ldbUpdate(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) uint64
if lv := ldbInsert(batch, repo, node, name, f); lv > maxLocalVer {
maxLocalVer = lv
}
- ldbUpdateGlobal(snap, batch, repo, node, name, f.Version)
+ if f.IsInvalid() {
+ ldbRemoveFromGlobal(snap, batch, repo, node, name)
+ } else {
+ ldbUpdateGlobal(snap, batch, repo, node, name, f.Version)
+ }
continue
}
@@ -289,11 +303,17 @@ func ldbUpdate(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) uint64
if err != nil {
panic(err)
}
- if ef.Version != f.Version {
+ // Flags might change without the version being bumped when we set the
+ // invalid flag on an existing file.
+ if ef.Version != f.Version || ef.Flags != f.Flags {
if lv := ldbInsert(batch, repo, node, name, f); lv > maxLocalVer {
maxLocalVer = lv
}
- ldbUpdateGlobal(snap, batch, repo, node, name, f.Version)
+ if f.IsInvalid() {
+ ldbRemoveFromGlobal(snap, batch, repo, node, name)
+ } else {
+ ldbUpdateGlobal(snap, batch, repo, node, name, f.Version)
+ }
}
}
@@ -385,7 +405,9 @@ func ldbRemoveFromGlobal(db dbReader, batch dbWriter, repo, node, file []byte) {
gk := globalKey(repo, file)
svl, err := db.Get(gk, nil)
if err != nil {
- panic(err)
+ // We might be called to "remove" a global version that doesn't exist
+ // if the first update for the file is already marked invalid.
+ return
}
var fl versionList
diff --git a/files/set_test.go b/files/set_test.go
index 8f3a9b316..aa88ee65c 100644
--- a/files/set_test.go
+++ b/files/set_test.go
@@ -86,7 +86,7 @@ func (l fileList) String() string {
var b bytes.Buffer
b.WriteString("[]protocol.FileList{\n")
for _, f := range l {
- fmt.Fprintf(&b, " %q: #%d, %d bytes, %d blocks\n", f.Name, f.Version, f.Size(), len(f.Blocks))
+ fmt.Fprintf(&b, " %q: #%d, %d bytes, %d blocks, flags=%o\n", f.Name, f.Version, f.Size(), len(f.Blocks), f.Flags)
}
b.WriteString("}")
return b.String()
@@ -280,6 +280,86 @@ func TestNeedWithInvalid(t *testing.T) {
}
}
+func TestUpdateToInvalid(t *testing.T) {
+ lamport.Default = lamport.Clock{}
+
+ db, err := leveldb.Open(storage.NewMemStorage(), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ s := files.NewSet("test", db)
+
+ localHave := fileList{
+ protocol.FileInfo{Name: "a", Version: 1000, Blocks: genBlocks(1)},
+ protocol.FileInfo{Name: "b", Version: 1001, Blocks: genBlocks(2)},
+ protocol.FileInfo{Name: "c", Version: 1002, Blocks: genBlocks(5), Flags: protocol.FlagInvalid},
+ protocol.FileInfo{Name: "d", Version: 1003, Blocks: genBlocks(7)},
+ }
+
+ s.ReplaceWithDelete(protocol.LocalNodeID, localHave)
+
+ have := fileList(haveList(s, protocol.LocalNodeID))
+ sort.Sort(have)
+
+ if fmt.Sprint(have) != fmt.Sprint(localHave) {
+ t.Errorf("Have incorrect before invalidation;\n A: %v !=\n E: %v", have, localHave)
+ }
+
+ localHave[1] = protocol.FileInfo{Name: "b", Version: 1001, Flags: protocol.FlagInvalid}
+ s.Update(protocol.LocalNodeID, localHave[1:2])
+
+ have = fileList(haveList(s, protocol.LocalNodeID))
+ sort.Sort(have)
+
+ if fmt.Sprint(have) != fmt.Sprint(localHave) {
+ t.Errorf("Have incorrect after invalidation;\n A: %v !=\n E: %v", have, localHave)
+ }
+}
+
+func TestInvalidAvailability(t *testing.T) {
+ lamport.Default = lamport.Clock{}
+
+ db, err := leveldb.Open(storage.NewMemStorage(), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ s := files.NewSet("test", db)
+
+ remote0Have := fileList{
+ protocol.FileInfo{Name: "both", Version: 1001, Blocks: genBlocks(2)},
+ protocol.FileInfo{Name: "r1only", Version: 1002, Blocks: genBlocks(5), Flags: protocol.FlagInvalid},
+ protocol.FileInfo{Name: "r0only", Version: 1003, Blocks: genBlocks(7)},
+ protocol.FileInfo{Name: "none", Version: 1004, Blocks: genBlocks(5), Flags: protocol.FlagInvalid},
+ }
+ remote1Have := fileList{
+ protocol.FileInfo{Name: "both", Version: 1001, Blocks: genBlocks(2)},
+ protocol.FileInfo{Name: "r1only", Version: 1002, Blocks: genBlocks(7)},
+ protocol.FileInfo{Name: "r0only", Version: 1003, Blocks: genBlocks(5), Flags: protocol.FlagInvalid},
+ protocol.FileInfo{Name: "none", Version: 1004, Blocks: genBlocks(5), Flags: protocol.FlagInvalid},
+ }
+
+ s.Replace(remoteNode0, remote0Have)
+ s.Replace(remoteNode1, remote1Have)
+
+ if av := s.Availability("both"); len(av) != 2 {
+ t.Error("Incorrect availability for 'both':", av)
+ }
+
+ if av := s.Availability("r0only"); len(av) != 1 || av[0] != remoteNode0 {
+ t.Error("Incorrect availability for 'r0only':", av)
+ }
+
+ if av := s.Availability("r1only"); len(av) != 1 || av[0] != remoteNode1 {
+ t.Error("Incorrect availability for 'r1only':", av)
+ }
+
+ if av := s.Availability("none"); len(av) != 0 {
+ t.Error("Incorrect availability for 'none':", av)
+ }
+}
+
func TestLocalDeleted(t *testing.T) {
db, err := leveldb.Open(storage.NewMemStorage(), nil)
if err != nil {
diff --git a/gui/app.js b/gui/app.js
index 3617e8d08..511bddb11 100644
--- a/gui/app.js
+++ b/gui/app.js
@@ -971,9 +971,9 @@ function debounce(func, wait) {
} else {
timeout = null;
if (again) {
+ again = false;
result = func.apply(context, args);
context = args = null;
- again = false;
}
}
};
diff --git a/ignore/ignore.go b/ignore/ignore.go
new file mode 100644
index 000000000..f2809e807
--- /dev/null
+++ b/ignore/ignore.go
@@ -0,0 +1,146 @@
+// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
+// All rights reserved. Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file.
+
+package ignore
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/syncthing/syncthing/fnmatch"
+)
+
+type Pattern struct {
+ match *regexp.Regexp
+ include bool
+}
+
+type Patterns []Pattern
+
+func Load(file string) (Patterns, error) {
+ seen := make(map[string]bool)
+ return loadIgnoreFile(file, seen)
+}
+
+func Parse(r io.Reader, file string) (Patterns, error) {
+ seen := map[string]bool{
+ file: true,
+ }
+ return parseIgnoreFile(r, file, seen)
+}
+
+func (l Patterns) Match(file string) bool {
+ for _, pattern := range l {
+ if pattern.match.MatchString(file) {
+ return pattern.include
+ }
+ }
+ return false
+}
+
+func loadIgnoreFile(file string, seen map[string]bool) (Patterns, 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) (Patterns, error) {
+ var exps Patterns
+
+ addPattern := func(line string) error {
+ include := true
+ if strings.HasPrefix(line, "!") {
+ line = line[1:]
+ include = false
+ }
+
+ if strings.HasPrefix(line, "/") {
+ // Pattern is rooted in the current dir only
+ exp, err := fnmatch.Convert(line[1:], fnmatch.FNM_PATHNAME)
+ if err != nil {
+ return fmt.Errorf("Invalid pattern %q in ignore file", line)
+ }
+ exps = append(exps, 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, fnmatch.FNM_PATHNAME)
+ if err != nil {
+ return fmt.Errorf("Invalid pattern %q in ignore file", line)
+ }
+ exps = append(exps, Pattern{exp, include})
+
+ exp, err = fnmatch.Convert(line[3:], fnmatch.FNM_PATHNAME)
+ if err != nil {
+ return fmt.Errorf("Invalid pattern %q in ignore file", line)
+ }
+ exps = append(exps, Pattern{exp, include})
+ } else if strings.HasPrefix(line, "#include ") {
+ includeFile := filepath.Join(filepath.Dir(currentFile), line[len("#include "):])
+ includes, err := loadIgnoreFile(includeFile, seen)
+ if err != nil {
+ return err
+ } else {
+ exps = append(exps, includes...)
+ }
+ } else {
+ // Path name or pattern, add it so it matches files both in
+ // current directory and subdirs.
+ exp, err := fnmatch.Convert(line, fnmatch.FNM_PATHNAME)
+ if err != nil {
+ return fmt.Errorf("Invalid pattern %q in ignore file", line)
+ }
+ exps = append(exps, Pattern{exp, include})
+
+ exp, err = fnmatch.Convert("**/"+line, fnmatch.FNM_PATHNAME)
+ if err != nil {
+ return fmt.Errorf("Invalid pattern %q in ignore file", line)
+ }
+ exps = append(exps, Pattern{exp, include})
+ }
+ return nil
+ }
+
+ scanner := bufio.NewScanner(fd)
+ var err error
+ for scanner.Scan() {
+ line := strings.TrimSpace(scanner.Text())
+ switch {
+ case 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 exps, nil
+}
diff --git a/ignore/ignore_test.go b/ignore/ignore_test.go
new file mode 100644
index 000000000..0d3d56ec4
--- /dev/null
+++ b/ignore/ignore_test.go
@@ -0,0 +1,104 @@
+package ignore_test
+
+import (
+ "bytes"
+ "path/filepath"
+ "testing"
+
+ "github.com/syncthing/syncthing/ignore"
+)
+
+func TestIgnore(t *testing.T) {
+ pats, err := ignore.Load("testdata/.stignore")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var tests = []struct {
+ f string
+ r bool
+ }{
+ {"afile", false},
+ {"bfile", true},
+ {"cfile", false},
+ {"dfile", false},
+ {"efile", true},
+ {"ffile", true},
+
+ {"dir1", false},
+ {filepath.Join("dir1", "cfile"), true},
+ {filepath.Join("dir1", "dfile"), false},
+ {filepath.Join("dir1", "efile"), true},
+ {filepath.Join("dir1", "ffile"), false},
+
+ {"dir2", false},
+ {filepath.Join("dir2", "cfile"), false},
+ {filepath.Join("dir2", "dfile"), true},
+ {filepath.Join("dir2", "efile"), true},
+ {filepath.Join("dir2", "ffile"), false},
+
+ {filepath.Join("dir3"), true},
+ {filepath.Join("dir3", "afile"), true},
+ }
+
+ for i, tc := range tests {
+ if r := pats.Match(tc.f); r != tc.r {
+ t.Errorf("Incorrect ignoreFile() #%d (%s); E: %v, A: %v", i, tc.f, tc.r, r)
+ }
+ }
+}
+
+func TestExcludes(t *testing.T) {
+ stignore := `
+ !iex2
+ !ign1/ex
+ ign1
+ i*2
+ !ign2
+ `
+ pats, err := ignore.Parse(bytes.NewBufferString(stignore), ".stignore")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var tests = []struct {
+ f string
+ r bool
+ }{
+ {"ign1", true},
+ {"ign2", true},
+ {"ibla2", true},
+ {"iex2", false},
+ {"ign1/ign", true},
+ {"ign1/ex", false},
+ {"ign1/iex2", false},
+ {"iex2/ign", false},
+ {"foo/bar/ign1", true},
+ {"foo/bar/ign2", true},
+ {"foo/bar/iex2", false},
+ }
+
+ for _, tc := range tests {
+ if r := pats.Match(tc.f); r != tc.r {
+ t.Errorf("Incorrect match for %s: %v != %v", tc.f, r, tc.r)
+ }
+ }
+}
+
+func TestBadPatterns(t *testing.T) {
+ var badPatterns = []string{
+ "[",
+ "/[",
+ "**/[",
+ "#include nonexistent",
+ "#include .stignore",
+ "!#include makesnosense",
+ }
+
+ for _, pat := range badPatterns {
+ parsed, err := ignore.Parse(bytes.NewBufferString(pat), ".stignore")
+ if err == nil {
+ t.Errorf("No error for pattern %q: %v", pat, parsed)
+ }
+ }
+}
diff --git a/ignore/testdata/.stignore b/ignore/testdata/.stignore
new file mode 100644
index 000000000..89a11f4b8
--- /dev/null
+++ b/ignore/testdata/.stignore
@@ -0,0 +1,6 @@
+#include excludes
+
+bfile
+dir1/cfile
+**/efile
+/ffile
diff --git a/ignore/testdata/dir3/cfile b/ignore/testdata/dir3/cfile
new file mode 100644
index 000000000..76018072e
--- /dev/null
+++ b/ignore/testdata/dir3/cfile
@@ -0,0 +1 @@
+baz
diff --git a/ignore/testdata/dir3/dfile b/ignore/testdata/dir3/dfile
new file mode 100644
index 000000000..d90bda0ff
--- /dev/null
+++ b/ignore/testdata/dir3/dfile
@@ -0,0 +1 @@
+quux
diff --git a/ignore/testdata/excludes b/ignore/testdata/excludes
new file mode 100644
index 000000000..679462048
--- /dev/null
+++ b/ignore/testdata/excludes
@@ -0,0 +1,2 @@
+dir2/dfile
+#include further-excludes
diff --git a/ignore/testdata/further-excludes b/ignore/testdata/further-excludes
new file mode 100644
index 000000000..9e831d51b
--- /dev/null
+++ b/ignore/testdata/further-excludes
@@ -0,0 +1 @@
+dir3
diff --git a/integration/all.sh b/integration/all.sh
index 0ffec5844..809b3e45b 100755
--- a/integration/all.sh
+++ b/integration/all.sh
@@ -2,7 +2,7 @@
set -euo pipefail
IFS=$'\n\t'
-go test -tags integration -v
+#go test -tags integration -v
./test-http.sh
./test-merge.sh
./test-delupd.sh
diff --git a/model/model.go b/model/model.go
index 548127af8..8a5984fa7 100644
--- a/model/model.go
+++ b/model/model.go
@@ -19,6 +19,7 @@ import (
"github.com/syncthing/syncthing/config"
"github.com/syncthing/syncthing/events"
"github.com/syncthing/syncthing/files"
+ "github.com/syncthing/syncthing/ignore"
"github.com/syncthing/syncthing/lamport"
"github.com/syncthing/syncthing/protocol"
"github.com/syncthing/syncthing/scanner"
@@ -72,6 +73,7 @@ type Model struct {
repoNodes map[string][]protocol.NodeID // repo -> nodeIDs
nodeRepos map[protocol.NodeID][]string // nodeID -> repos
nodeStatRefs map[protocol.NodeID]*stats.NodeStatisticsReference // nodeID -> statsRef
+ repoIgnores map[string]ignore.Patterns // repo -> list of ignore patterns
rmut sync.RWMutex // protects the above
repoState map[string]repoState // repo -> state
@@ -108,6 +110,7 @@ func NewModel(indexDir string, cfg *config.Configuration, nodeName, clientName,
repoNodes: make(map[string][]protocol.NodeID),
nodeRepos: make(map[protocol.NodeID][]string),
nodeStatRefs: make(map[protocol.NodeID]*stats.NodeStatisticsReference),
+ repoIgnores: make(map[string]ignore.Patterns),
repoState: make(map[string]repoState),
repoStateChanged: make(map[string]time.Time),
protoConn: make(map[protocol.NodeID]protocol.Connection),
@@ -289,6 +292,9 @@ func (m *Model) LocalSize(repo string) (files, deleted int, bytes int64) {
defer m.rmut.RUnlock()
if rf, ok := m.repoFiles[repo]; ok {
rf.WithHaveTruncated(protocol.LocalNodeID, func(f protocol.FileIntf) bool {
+ if f.IsInvalid() {
+ return true
+ }
fs, de, by := sizeOfFile(f)
files += fs
deleted += de
@@ -348,24 +354,32 @@ func (m *Model) Index(nodeID protocol.NodeID, repo string, fs []protocol.FileInf
return
}
- for i := range fs {
- lamport.Default.Tick(fs[i].Version)
- }
-
m.rmut.RLock()
- r, ok := m.repoFiles[repo]
+ files, ok := m.repoFiles[repo]
+ ignores, _ := m.repoIgnores[repo]
m.rmut.RUnlock()
- if ok {
- r.Replace(nodeID, fs)
- } else {
+
+ if !ok {
l.Fatalf("Index for nonexistant repo %q", repo)
}
+ for i := 0; i < len(fs); {
+ lamport.Default.Tick(fs[i].Version)
+ if ignores.Match(fs[i].Name) {
+ fs[i] = fs[len(fs)-1]
+ fs = fs[:len(fs)-1]
+ } else {
+ i++
+ }
+ }
+
+ files.Replace(nodeID, fs)
+
events.Default.Log(events.RemoteIndexUpdated, map[string]interface{}{
"node": nodeID.String(),
"repo": repo,
"items": len(fs),
- "version": r.LocalVersion(nodeID),
+ "version": files.LocalVersion(nodeID),
})
}
@@ -381,24 +395,32 @@ func (m *Model) IndexUpdate(nodeID protocol.NodeID, repo string, fs []protocol.F
return
}
- for i := range fs {
- lamport.Default.Tick(fs[i].Version)
- }
-
m.rmut.RLock()
- r, ok := m.repoFiles[repo]
+ files, ok := m.repoFiles[repo]
+ ignores, _ := m.repoIgnores[repo]
m.rmut.RUnlock()
- if ok {
- r.Update(nodeID, fs)
- } else {
+
+ if !ok {
l.Fatalf("IndexUpdate for nonexistant repo %q", repo)
}
+ for i := 0; i < len(fs); {
+ lamport.Default.Tick(fs[i].Version)
+ if ignores.Match(fs[i].Name) {
+ fs[i] = fs[len(fs)-1]
+ fs = fs[:len(fs)-1]
+ } else {
+ i++
+ }
+ }
+
+ files.Update(nodeID, fs)
+
events.Default.Log(events.RemoteIndexUpdated, map[string]interface{}{
"node": nodeID.String(),
"repo": repo,
"items": len(fs),
- "version": r.LocalVersion(nodeID),
+ "version": files.LocalVersion(nodeID),
})
}
@@ -572,7 +594,7 @@ func (m *Model) AddConnection(rawConn io.Closer, protoConn protocol.Connection)
m.rmut.RLock()
for _, repo := range m.nodeRepos[nodeID] {
fs := m.repoFiles[repo]
- go sendIndexes(protoConn, repo, fs)
+ go sendIndexes(protoConn, repo, fs, m.repoIgnores[repo])
}
if statRef, ok := m.nodeStatRefs[nodeID]; ok {
statRef.WasSeen()
@@ -583,7 +605,7 @@ func (m *Model) AddConnection(rawConn io.Closer, protoConn protocol.Connection)
m.pmut.Unlock()
}
-func sendIndexes(conn protocol.Connection, repo string, fs *files.Set) {
+func sendIndexes(conn protocol.Connection, repo string, fs *files.Set, ignores ignore.Patterns) {
nodeID := conn.ID()
name := conn.Name()
var err error
@@ -598,7 +620,7 @@ func sendIndexes(conn protocol.Connection, repo string, fs *files.Set) {
}
}()
- minLocalVer, err := sendIndexTo(true, 0, conn, repo, fs)
+ minLocalVer, err := sendIndexTo(true, 0, conn, repo, fs, ignores)
for err == nil {
time.Sleep(5 * time.Second)
@@ -606,11 +628,11 @@ func sendIndexes(conn protocol.Connection, repo string, fs *files.Set) {
continue
}
- minLocalVer, err = sendIndexTo(false, minLocalVer, conn, repo, fs)
+ minLocalVer, err = sendIndexTo(false, minLocalVer, conn, repo, fs, ignores)
}
}
-func sendIndexTo(initial bool, minLocalVer uint64, conn protocol.Connection, repo string, fs *files.Set) (uint64, error) {
+func sendIndexTo(initial bool, minLocalVer uint64, conn protocol.Connection, repo string, fs *files.Set, ignores ignore.Patterns) (uint64, error) {
nodeID := conn.ID()
name := conn.Name()
batch := make([]protocol.FileInfo, 0, indexBatchSize)
@@ -628,6 +650,10 @@ func sendIndexTo(initial bool, minLocalVer uint64, conn protocol.Connection, rep
maxLocalVer = f.LocalVersion
}
+ if ignores.Match(f.Name) {
+ return true
+ }
+
if len(batch) == indexBatchSize || currentBatchSize > indexTargetSize {
if initial {
if err = conn.Index(repo, batch); err != nil {
@@ -781,10 +807,13 @@ func (m *Model) ScanRepoSub(repo, sub string) error {
fs, ok := m.repoFiles[repo]
dir := m.repoCfgs[repo].Directory
+ ignores, _ := ignore.Load(filepath.Join(dir, ".stignore"))
+ m.repoIgnores[repo] = ignores
+
w := &scanner.Walker{
Dir: dir,
Sub: sub,
- IgnoreFile: ".stignore",
+ Ignores: ignores,
BlockSize: scanner.StandardBlockSize,
TempNamer: defTempNamer,
CurrentFiler: cFiler{m, repo},
@@ -827,15 +856,40 @@ func (m *Model) ScanRepoSub(repo, sub string) error {
fs.WithHaveTruncated(protocol.LocalNodeID, func(fi protocol.FileIntf) bool {
f := fi.(protocol.FileInfoTruncated)
if !strings.HasPrefix(f.Name, sub) {
+ // Return true so that we keep iterating, until we get to the part
+ // of the tree we are interested in. Then return false so we stop
+ // iterating when we've passed the end of the subtree.
return !seenPrefix
}
+
seenPrefix = true
if !protocol.IsDeleted(f.Flags) {
+ if f.IsInvalid() {
+ return true
+ }
+
if len(batch) == batchSize {
fs.Update(protocol.LocalNodeID, batch)
batch = batch[:0]
}
- if _, err := os.Stat(filepath.Join(dir, f.Name)); err != nil && os.IsNotExist(err) {
+
+ if ignores.Match(f.Name) {
+ // File has been ignored. Set invalid bit.
+ nf := protocol.FileInfo{
+ Name: f.Name,
+ Flags: f.Flags | protocol.FlagInvalid,
+ Modified: f.Modified,
+ Version: f.Version, // The file is still the same, so don't bump version
+ }
+ events.Default.Log(events.LocalIndexUpdated, map[string]interface{}{
+ "repo": repo,
+ "name": f.Name,
+ "modified": time.Unix(f.Modified, 0),
+ "flags": fmt.Sprintf("0%o", f.Flags),
+ "size": f.Size(),
+ })
+ batch = append(batch, nf)
+ } else if _, err := os.Stat(filepath.Join(dir, f.Name)); err != nil && os.IsNotExist(err) {
// File has been deleted
nf := protocol.FileInfo{
Name: f.Name,
@@ -928,6 +982,7 @@ func (m *Model) Override(repo string) {
fs := m.repoFiles[repo]
m.rmut.RUnlock()
+ m.setState(repo, RepoScanning)
batch := make([]protocol.FileInfo, 0, indexBatchSize)
fs.WithNeed(protocol.LocalNodeID, func(fi protocol.FileIntf) bool {
need := fi.(protocol.FileInfo)
@@ -953,6 +1008,7 @@ func (m *Model) Override(repo string) {
if len(batch) > 0 {
fs.Update(protocol.LocalNodeID, batch)
}
+ m.setState(repo, RepoIdle)
}
// Version returns the change version for the given repository. This is
diff --git a/scanner/testdata/.stignore b/scanner/testdata/.stignore
index d8d9cd382..8d7a8adeb 100644
--- a/scanner/testdata/.stignore
+++ b/scanner/testdata/.stignore
@@ -1,5 +1,4 @@
#include excludes
-#include nonexistent-file
bfile
dir1/cfile
diff --git a/scanner/testdata/excludes b/scanner/testdata/excludes
index b6de3b738..679462048 100644
--- a/scanner/testdata/excludes
+++ b/scanner/testdata/excludes
@@ -1,4 +1,2 @@
dir2/dfile
-#include excludes
#include further-excludes
-#include loop-excludes
diff --git a/scanner/testdata/loop-excludes b/scanner/testdata/loop-excludes
deleted file mode 100644
index 9337e0d49..000000000
--- a/scanner/testdata/loop-excludes
+++ /dev/null
@@ -1 +0,0 @@
-#include excludes
diff --git a/scanner/walk.go b/scanner/walk.go
index 1278490c0..49fc90781 100644
--- a/scanner/walk.go
+++ b/scanner/walk.go
@@ -5,19 +5,14 @@
package scanner
import (
- "bufio"
"errors"
- "io"
"os"
- "path"
"path/filepath"
- "regexp"
"runtime"
- "strings"
"code.google.com/p/go.text/unicode/norm"
- "github.com/syncthing/syncthing/fnmatch"
+ "github.com/syncthing/syncthing/ignore"
"github.com/syncthing/syncthing/lamport"
"github.com/syncthing/syncthing/protocol"
)
@@ -29,8 +24,8 @@ type Walker struct {
Sub string
// BlockSize controls the size of the block used when hashing.
BlockSize int
- // If IgnoreFile is not empty, it is the name used for the file that holds ignore patterns.
- IgnoreFile string
+ // List of patterns to ignore
+ Ignores ignore.Patterns
// If TempNamer is not nil, it is used to ignore tempory files when walking.
TempNamer TempNamer
// If CurrentFiler is not nil, it is queried for the current file before rescanning.
@@ -57,7 +52,7 @@ type CurrentFiler interface {
// file system. Files are blockwise hashed.
func (w *Walker) Walk() (chan protocol.FileInfo, error) {
if debug {
- l.Debugln("Walk", w.Dir, w.Sub, w.BlockSize, w.IgnoreFile)
+ l.Debugln("Walk", w.Dir, w.Sub, w.BlockSize, w.Ignores)
}
err := checkDir(w.Dir)
@@ -69,11 +64,8 @@ func (w *Walker) Walk() (chan protocol.FileInfo, error) {
hashedFiles := make(chan protocol.FileInfo)
newParallelHasher(w.Dir, w.BlockSize, runtime.NumCPU(), hashedFiles, files)
- var ignores []*regexp.Regexp
go func() {
- filepath.Walk(w.Dir, w.loadIgnoreFiles(w.Dir, &ignores))
-
- hashFiles := w.walkAndHashFiles(files, ignores)
+ hashFiles := w.walkAndHashFiles(files)
filepath.Walk(filepath.Join(w.Dir, w.Sub), hashFiles)
close(files)
}()
@@ -86,113 +78,7 @@ func (w *Walker) CleanTempFiles() {
filepath.Walk(w.Dir, w.cleanTempFile)
}
-func (w *Walker) loadIgnoreFiles(dir string, ignores *[]*regexp.Regexp) filepath.WalkFunc {
- return func(p string, info os.FileInfo, err error) error {
- if err != nil {
- return nil
- }
-
- rn, err := filepath.Rel(dir, p)
- if err != nil {
- return nil
- }
-
- if pn, sn := filepath.Split(rn); sn == w.IgnoreFile {
- pn := filepath.Clean(pn)
- filesSeen := make(map[string]map[string]bool)
- dirIgnores := loadIgnoreFile(p, pn, filesSeen)
- *ignores = append(*ignores, dirIgnores...)
- }
-
- return nil
- }
-}
-
-func loadIgnoreFile(ignFile, base string, filesSeen map[string]map[string]bool) []*regexp.Regexp {
- fd, err := os.Open(ignFile)
- if err != nil {
- return nil
- }
- defer fd.Close()
- return parseIgnoreFile(fd, base, ignFile, filesSeen)
-}
-
-func parseIgnoreFile(fd io.Reader, base, currentFile string, filesSeen map[string]map[string]bool) []*regexp.Regexp {
- var exps []*regexp.Regexp
- scanner := bufio.NewScanner(fd)
- for scanner.Scan() {
- line := strings.TrimSpace(scanner.Text())
- if line == "" {
- continue
- }
-
- if strings.HasPrefix(line, "/") {
- // Pattern is rooted in the current dir only
- exp, err := fnmatch.Convert(path.Join(base, line[1:]), fnmatch.FNM_PATHNAME)
- if err != nil {
- l.Warnf("Invalid pattern %q in ignore file", line)
- continue
- }
- exps = append(exps, exp)
- } else if strings.HasPrefix(line, "**/") {
- // Add the pattern as is, and without **/ so it matches in current dir
- exp, err := fnmatch.Convert(line, fnmatch.FNM_PATHNAME)
- if err != nil {
- l.Warnf("Invalid pattern %q in ignore file", line)
- continue
- }
- exps = append(exps, exp)
-
- exp, err = fnmatch.Convert(path.Join(base, line[3:]), fnmatch.FNM_PATHNAME)
- if err != nil {
- l.Warnf("Invalid pattern %q in ignore file", line)
- continue
- }
- exps = append(exps, exp)
- } else if strings.HasPrefix(line, "#include ") {
- includeFile := filepath.Join(filepath.Dir(currentFile), strings.Replace(line, "#include ", "", 1))
- if _, err := os.Stat(includeFile); os.IsNotExist(err) {
- l.Infoln("Could not open ignore include file", includeFile)
- } else {
- seen := false
- if seenByCurrent, ok := filesSeen[currentFile]; ok {
- _, seen = seenByCurrent[includeFile]
- }
-
- if seen {
- l.Warnf("Recursion detected while including %s from %s", includeFile, currentFile)
- } else {
- if filesSeen[currentFile] == nil {
- filesSeen[currentFile] = make(map[string]bool)
- }
- filesSeen[currentFile][includeFile] = true
- includes := loadIgnoreFile(includeFile, base, filesSeen)
- exps = append(exps, includes...)
- }
- }
- } else {
- // Path name or pattern, add it so it matches files both in
- // current directory and subdirs.
- exp, err := fnmatch.Convert(path.Join(base, line), fnmatch.FNM_PATHNAME)
- if err != nil {
- l.Warnf("Invalid pattern %q in ignore file", line)
- continue
- }
- exps = append(exps, exp)
-
- exp, err = fnmatch.Convert(path.Join(base, "**", line), fnmatch.FNM_PATHNAME)
- if err != nil {
- l.Warnf("Invalid pattern %q in ignore file", line)
- continue
- }
- exps = append(exps, exp)
- }
- }
-
- return exps
-}
-
-func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo, ignores []*regexp.Regexp) filepath.WalkFunc {
+func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFunc {
return func(p string, info os.FileInfo, err error) error {
if err != nil {
if debug {
@@ -221,7 +107,7 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo, ignores []*regex
return nil
}
- if sn := filepath.Base(rn); sn == w.IgnoreFile || sn == ".stversions" || w.ignoreFile(ignores, rn) {
+ if sn := filepath.Base(rn); sn == ".stignore" || sn == ".stversions" || w.Ignores.Match(rn) {
// An ignored file
if debug {
l.Debugln("ignored:", rn)
@@ -305,18 +191,6 @@ func (w *Walker) cleanTempFile(path string, info os.FileInfo, err error) error {
return nil
}
-func (w *Walker) ignoreFile(patterns []*regexp.Regexp, file string) bool {
- for _, pattern := range patterns {
- if pattern.MatchString(file) {
- if debug {
- l.Debugf("%q matches %v", file, pattern)
- }
- return true
- }
- }
- return false
-}
-
func checkDir(dir string) error {
if info, err := os.Lstat(dir); err != nil {
return err
diff --git a/scanner/walk_test.go b/scanner/walk_test.go
index 7a9f705dc..cb9960947 100644
--- a/scanner/walk_test.go
+++ b/scanner/walk_test.go
@@ -13,6 +13,7 @@ import (
"sort"
"testing"
+ "github.com/syncthing/syncthing/ignore"
"github.com/syncthing/syncthing/protocol"
)
@@ -30,9 +31,8 @@ var testdata = testfileList{
{filepath.Join("dir1", "dfile"), 5, "49ae93732fcf8d63fe1cce759664982dbd5b23161f007dba8561862adc96d063"},
{"dir2", 128, ""},
{filepath.Join("dir2", "cfile"), 4, "bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c"},
- {"excludes", 78, "1f5ac95d9e6fb2516629a029d788d27953c7bb2f4dc09184b660fdda0c8f2f04"},
+ {"excludes", 37, "df90b52f0c55dba7a7a940affe482571563b1ac57bd5be4d8a0291e7de928e06"},
{"further-excludes", 5, "7eb0a548094fa6295f7fd9200d69973e5f5ec5c04f2a86d998080ac43ecf89f1"},
- {"loop-excludes", 18, "2db057aa82a8b8fe4b1367ccc875259ed4b8020255820d4e3d4bfe78f0dd3f2a"},
}
var correctIgnores = map[string][]string{
@@ -47,11 +47,16 @@ func init() {
}
func TestWalkSub(t *testing.T) {
+ ignores, err := ignore.Load("testdata/.stignore")
+ if err != nil {
+ t.Fatal(err)
+ }
+
w := Walker{
- Dir: "testdata",
- Sub: "dir2",
- BlockSize: 128 * 1024,
- IgnoreFile: ".stignore",
+ Dir: "testdata",
+ Sub: "dir2",
+ BlockSize: 128 * 1024,
+ Ignores: ignores,
}
fchan, err := w.Walk()
var files []protocol.FileInfo
@@ -77,10 +82,16 @@ func TestWalkSub(t *testing.T) {
}
func TestWalk(t *testing.T) {
+ ignores, err := ignore.Load("testdata/.stignore")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Log(ignores)
+
w := Walker{
- Dir: "testdata",
- BlockSize: 128 * 1024,
- IgnoreFile: ".stignore",
+ Dir: "testdata",
+ BlockSize: 128 * 1024,
+ Ignores: ignores,
}
fchan, err := w.Walk()
@@ -102,9 +113,8 @@ func TestWalk(t *testing.T) {
func TestWalkError(t *testing.T) {
w := Walker{
- Dir: "testdata-missing",
- BlockSize: 128 * 1024,
- IgnoreFile: ".stignore",
+ Dir: "testdata-missing",
+ BlockSize: 128 * 1024,
}
_, err := w.Walk()
@@ -113,9 +123,8 @@ func TestWalkError(t *testing.T) {
}
w = Walker{
- Dir: "testdata/bar",
- BlockSize: 128 * 1024,
- IgnoreFile: ".stignore",
+ Dir: "testdata/bar",
+ BlockSize: 128 * 1024,
}
_, err = w.Walk()
@@ -124,67 +133,6 @@ func TestWalkError(t *testing.T) {
}
}
-func TestIgnore(t *testing.T) {
- patStr := bytes.NewBufferString(`
- t2
- /t3
- sub/dir/*
- */other/test
- **/deep
- `)
- patterns := parseIgnoreFile(patStr, "", "", make(map[string]map[string]bool))
-
- patStr = bytes.NewBufferString(`
- bar
- z*
- q[abc]x
- `)
- patterns = append(patterns, parseIgnoreFile(patStr, "foo", "", make(map[string]map[string]bool))...)
-
- patStr = bytes.NewBufferString(`
- quux
- .*
- `)
- patterns = append(patterns, parseIgnoreFile(patStr, "foo/baz", "", make(map[string]map[string]bool))...)
-
- var tests = []struct {
- f string
- r bool
- }{
- {filepath.Join("foo", "bar"), true},
- {filepath.Join("t3"), true},
- {filepath.Join("foofoo"), false},
- {filepath.Join("foo", "quux"), false},
- {filepath.Join("foo", "zuux"), true},
- {filepath.Join("foo", "qzuux"), false},
- {filepath.Join("foo", "baz", "t1"), false},
- {filepath.Join("foo", "baz", "t2"), true},
- {filepath.Join("foo", "baz", "t3"), false},
- {filepath.Join("foo", "baz", "bar"), true},
- {filepath.Join("foo", "baz", "quuxa"), false},
- {filepath.Join("foo", "baz", "aquux"), false},
- {filepath.Join("foo", "baz", ".quux"), true},
- {filepath.Join("foo", "baz", "zquux"), true},
- {filepath.Join("foo", "baz", "quux"), true},
- {filepath.Join("foo", "bazz", "quux"), false},
- {filepath.Join("sub", "dir", "hej"), true},
- {filepath.Join("deeper", "sub", "dir", "hej"), true},
- {filepath.Join("other", "test"), false},
- {filepath.Join("sub", "other", "test"), true},
- {filepath.Join("deeper", "sub", "other", "test"), true},
- {filepath.Join("deep"), true},
- {filepath.Join("deeper", "deep"), true},
- {filepath.Join("deeper", "deeper", "deep"), true},
- }
-
- w := Walker{}
- for i, tc := range tests {
- if r := w.ignoreFile(patterns, tc.f); r != tc.r {
- t.Errorf("Incorrect ignoreFile() #%d (%s); E: %v, A: %v", i, tc.f, tc.r, r)
- }
- }
-}
-
type fileList []protocol.FileInfo
func (f fileList) Len() int {