Ignore files matching patterns in .stignore (fixes #7)

This commit is contained in:
Jakob Borg 2014-01-06 21:17:18 +01:00
parent 46d828e349
commit 986b15573a
5 changed files with 132 additions and 21 deletions

View File

@ -186,6 +186,15 @@ $ syncthing --gui 127.0.0.1:8080
You then point your browser to the given address. You then point your browser to the given address.
Excluding Files
---------------
syncthing looks for files named `.stignore` while walking the
repository. The file is expected to contain glob patterns of file names
to ignore. Patterns are matched on file name only and apply to files in
the same directory as the `.stignore` file and in directories lower down
in the hierarchy.
License License
======= =======

View File

@ -333,7 +333,7 @@ func connect(myID string, addr string, nodeAddrs map[string][]string, m *model.M
} }
func updateLocalModel(m *model.Model) { func updateLocalModel(m *model.Model) {
files := m.Walk(!opts.NoSymlinks) files := m.FilteredWalk(!opts.NoSymlinks)
m.ReplaceLocal(files) m.ReplaceLocal(files)
saveIndex(m) saveIndex(m)
} }

View File

@ -58,7 +58,7 @@ func init() {
func TestUpdateLocal(t *testing.T) { func TestUpdateLocal(t *testing.T) {
m := NewModel("testdata") m := NewModel("testdata")
fs := m.Walk(false) fs, _ := m.Walk(false)
m.ReplaceLocal(fs) m.ReplaceLocal(fs)
if len(m.need) > 0 { if len(m.need) > 0 {
@ -100,7 +100,7 @@ func TestUpdateLocal(t *testing.T) {
func TestRemoteUpdateExisting(t *testing.T) { func TestRemoteUpdateExisting(t *testing.T) {
m := NewModel("testdata") m := NewModel("testdata")
fs := m.Walk(false) fs, _ := m.Walk(false)
m.ReplaceLocal(fs) m.ReplaceLocal(fs)
newFile := protocol.FileInfo{ newFile := protocol.FileInfo{
@ -117,7 +117,7 @@ func TestRemoteUpdateExisting(t *testing.T) {
func TestRemoteAddNew(t *testing.T) { func TestRemoteAddNew(t *testing.T) {
m := NewModel("testdata") m := NewModel("testdata")
fs := m.Walk(false) fs, _ := m.Walk(false)
m.ReplaceLocal(fs) m.ReplaceLocal(fs)
newFile := protocol.FileInfo{ newFile := protocol.FileInfo{
@ -134,7 +134,7 @@ func TestRemoteAddNew(t *testing.T) {
func TestRemoteUpdateOld(t *testing.T) { func TestRemoteUpdateOld(t *testing.T) {
m := NewModel("testdata") m := NewModel("testdata")
fs := m.Walk(false) fs, _ := m.Walk(false)
m.ReplaceLocal(fs) m.ReplaceLocal(fs)
oldTimeStamp := int64(1234) oldTimeStamp := int64(1234)
@ -152,7 +152,7 @@ func TestRemoteUpdateOld(t *testing.T) {
func TestRemoteIndexUpdate(t *testing.T) { func TestRemoteIndexUpdate(t *testing.T) {
m := NewModel("testdata") m := NewModel("testdata")
fs := m.Walk(false) fs, _ := m.Walk(false)
m.ReplaceLocal(fs) m.ReplaceLocal(fs)
foo := protocol.FileInfo{ foo := protocol.FileInfo{
@ -185,7 +185,7 @@ func TestRemoteIndexUpdate(t *testing.T) {
func TestDelete(t *testing.T) { func TestDelete(t *testing.T) {
m := NewModel("testdata") m := NewModel("testdata")
fs := m.Walk(false) fs, _ := m.Walk(false)
m.ReplaceLocal(fs) m.ReplaceLocal(fs)
if l1, l2 := len(m.local), len(fs); l1 != l2 { if l1, l2 := len(m.local), len(fs); l1 != l2 {
@ -275,7 +275,7 @@ func TestDelete(t *testing.T) {
func TestForgetNode(t *testing.T) { func TestForgetNode(t *testing.T) {
m := NewModel("testdata") m := NewModel("testdata")
fs := m.Walk(false) fs, _ := m.Walk(false)
m.ReplaceLocal(fs) m.ReplaceLocal(fs)
if l1, l2 := len(m.local), len(fs); l1 != l2 { if l1, l2 := len(m.local), len(fs); l1 != l2 {

View File

@ -1,7 +1,9 @@
package model package model
import ( import (
"bytes"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os" "os"
"path" "path"
@ -35,7 +37,7 @@ func tempName(name string, modified int64) string {
return path.Join(tdir, tname) return path.Join(tdir, tname)
} }
func (m *Model) genWalker(res *[]File) filepath.WalkFunc { func (m *Model) genWalker(res *[]File, ign map[string][]string) filepath.WalkFunc {
return func(p string, info os.FileInfo, err error) error { return func(p string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return nil return nil
@ -45,12 +47,26 @@ func (m *Model) genWalker(res *[]File) filepath.WalkFunc {
return nil return nil
} }
if info.Mode()&os.ModeType == 0 { rn, err := filepath.Rel(m.dir, p)
rn, err := filepath.Rel(m.dir, p) if err != nil {
if err != nil { return nil
return nil }
}
if pn, sn := path.Split(rn); sn == ".stignore" {
pn := strings.Trim(pn, "/")
bs, _ := ioutil.ReadFile(p)
lines := bytes.Split(bs, []byte("\n"))
var patterns []string
for _, line := range lines {
if len(line) > 0 {
patterns = append(patterns, string(line))
}
}
ign[pn] = patterns
return nil
}
if info.Mode()&os.ModeType == 0 {
fi, err := os.Stat(p) fi, err := os.Stat(p)
if err != nil { if err != nil {
return nil return nil
@ -94,21 +110,21 @@ func (m *Model) genWalker(res *[]File) filepath.WalkFunc {
// Walk returns the list of files found in the local repository by scanning the // Walk returns the list of files found in the local repository by scanning the
// file system. Files are blockwise hashed. // file system. Files are blockwise hashed.
func (m *Model) Walk(followSymlinks bool) []File { func (m *Model) Walk(followSymlinks bool) (files []File, ignore map[string][]string) {
var files []File ignore = make(map[string][]string)
fn := m.genWalker(&files) fn := m.genWalker(&files, ignore)
filepath.Walk(m.dir, fn) filepath.Walk(m.dir, fn)
if followSymlinks { if followSymlinks {
d, err := os.Open(m.dir) d, err := os.Open(m.dir)
if err != nil { if err != nil {
return files return
} }
defer d.Close() defer d.Close()
fis, err := d.Readdir(-1) fis, err := d.Readdir(-1)
if err != nil { if err != nil {
return files return
} }
for _, fi := range fis { for _, fi := range fis {
@ -118,7 +134,15 @@ func (m *Model) Walk(followSymlinks bool) []File {
} }
} }
return files return
}
// Walk returns the list of files found in the local repository by scanning the
// file system. Files are blockwise hashed. Patterns marked in .stignore files
// are removed from the results.
func (m *Model) FilteredWalk(followSymlinks bool) []File {
var files, ignored = m.Walk(followSymlinks)
return ignoreFilter(ignored, files)
} }
func (m *Model) cleanTempFile(path string, info os.FileInfo, err error) error { func (m *Model) cleanTempFile(path string, info os.FileInfo, err error) error {
@ -137,3 +161,21 @@ func (m *Model) cleanTempFile(path string, info os.FileInfo, err error) error {
func (m *Model) cleanTempFiles() { func (m *Model) cleanTempFiles() {
filepath.Walk(m.dir, m.cleanTempFile) filepath.Walk(m.dir, m.cleanTempFile)
} }
func ignoreFilter(patterns map[string][]string, files []File) (filtered []File) {
nextFile:
for _, f := range files {
first, last := path.Split(f.Name)
for prefix, pats := range patterns {
if len(prefix) == 0 || prefix == first || strings.HasPrefix(first, prefix+"/") {
for _, pattern := range pats {
if match, _ := path.Match(pattern, last); match {
continue nextFile
}
}
}
}
filtered = append(filtered, f)
}
return filtered
}

View File

@ -2,6 +2,7 @@ package model
import ( import (
"fmt" "fmt"
"reflect"
"testing" "testing"
"time" "time"
) )
@ -16,9 +17,13 @@ var testdata = []struct {
{"foo", 7, "aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f"}, {"foo", 7, "aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f"},
} }
var correctIgnores = map[string][]string{
"": {".*", "quux"},
}
func TestWalk(t *testing.T) { func TestWalk(t *testing.T) {
m := NewModel("testdata") m := NewModel("testdata")
files := m.Walk(false) files, ignores := m.Walk(false)
if l1, l2 := len(files), len(testdata); l1 != l2 { if l1, l2 := len(files), len(testdata); l1 != l2 {
t.Fatalf("Incorrect number of walked files %d != %d", l1, l2) t.Fatalf("Incorrect number of walked files %d != %d", l1, l2)
@ -39,4 +44,59 @@ func TestWalk(t *testing.T) {
t.Errorf("Unrealistic modtime %d for test %d", mt, i) t.Errorf("Unrealistic modtime %d for test %d", mt, i)
} }
} }
if !reflect.DeepEqual(ignores, correctIgnores) {
t.Errorf("Incorrect ignores\n %v\n %v", correctIgnores, ignores)
}
}
func TestFilteredWalk(t *testing.T) {
m := NewModel("testdata")
files := m.FilteredWalk(false)
if len(files) != 2 {
t.Fatalf("Incorrect number of walked filtered files %d != 2", len(files))
}
if files[0].Name != "bar" {
t.Error("Incorrect first file", files[0])
}
if files[1].Name != "foo" {
t.Error("Incorrect second file", files[1])
}
}
func TestIgnore(t *testing.T) {
var patterns = map[string][]string{
"": {"t2"},
"foo": {"bar", "z*"},
"foo/baz": {"quux", ".*"},
}
var files = []File{
{Name: "foo/bar"},
{Name: "foo/quux"},
{Name: "foo/zuux"},
{Name: "foo/qzuux"},
{Name: "foo/baz/t1"},
{Name: "foo/baz/t2"},
{Name: "foo/baz/bar"},
{Name: "foo/baz/quuxa"},
{Name: "foo/baz/aquux"},
{Name: "foo/baz/.quux"},
{Name: "foo/baz/zquux"},
{Name: "foo/baz/quux"},
{Name: "foo/bazz/quux"},
}
var remaining = []File{
{Name: "foo/quux"},
{Name: "foo/qzuux"},
{Name: "foo/baz/t1"},
{Name: "foo/baz/quuxa"},
{Name: "foo/baz/aquux"},
{Name: "foo/bazz/quux"},
}
var filtered = ignoreFilter(patterns, files)
if !reflect.DeepEqual(filtered, remaining) {
t.Errorf("Filtering mismatch\n %v\n %v", remaining, filtered)
}
} }