diff --git a/lib/config/folderconfiguration.go b/lib/config/folderconfiguration.go index 33efda739..a4c5a41a5 100644 --- a/lib/config/folderconfiguration.go +++ b/lib/config/folderconfiguration.go @@ -47,11 +47,11 @@ func (f FolderConfiguration) Filesystem() fs.Filesystem { // cfg.Folders["default"].Filesystem() should be valid. var opts []fs.Option if f.FilesystemType == fs.FilesystemTypeBasic && f.JunctionsAsDirs { - opts = append(opts, fs.WithJunctionsAsDirs()) + opts = append(opts, new(fs.OptionJunctionsAsDirs)) } filesystem := fs.NewFilesystem(f.FilesystemType, f.Path, opts...) if !f.CaseSensitiveFS { - filesystem = fs.NewCaseFilesystem(filesystem, opts...) + filesystem = fs.NewCaseFilesystem(filesystem) } return filesystem } diff --git a/lib/fs/basicfs.go b/lib/fs/basicfs.go index 242e658a3..2cf4b28b1 100644 --- a/lib/fs/basicfs.go +++ b/lib/fs/basicfs.go @@ -26,24 +26,26 @@ var ( errNotRelative = errors.New("not a relative path") ) -func WithJunctionsAsDirs() Option { - return Option{ - apply: func(fs Filesystem) { - if basic, ok := fs.(*BasicFilesystem); !ok { - l.Warnln("WithJunctionsAsDirs must only be used with FilesystemTypeBasic") - } else { - basic.junctionsAsDirs = true - } - }, - id: "junctionsAsDirs", +type OptionJunctionsAsDirs struct{} + +func (o *OptionJunctionsAsDirs) apply(fs Filesystem) { + if basic, ok := fs.(*BasicFilesystem); !ok { + l.Warnln("WithJunctionsAsDirs must only be used with FilesystemTypeBasic") + } else { + basic.junctionsAsDirs = true } } +func (o *OptionJunctionsAsDirs) String() string { + return "junctionsAsDirs" +} + // The BasicFilesystem implements all aspects by delegating to package os. // All paths are relative to the root and cannot (should not) escape the root directory. type BasicFilesystem struct { root string junctionsAsDirs bool + options []Option } func newBasicFilesystem(root string, opts ...Option) *BasicFilesystem { @@ -82,7 +84,8 @@ func newBasicFilesystem(root string, opts ...Option) *BasicFilesystem { } fs := &BasicFilesystem{ - root: root, + root: root, + options: opts, } for _, opt := range opts { opt.apply(fs) @@ -311,6 +314,10 @@ func (f *BasicFilesystem) URI() string { return strings.TrimPrefix(f.root, `\\?\`) } +func (f *BasicFilesystem) Options() []Option { + return f.options +} + func (f *BasicFilesystem) SameFile(fi1, fi2 FileInfo) bool { // Like os.SameFile, we always return false unless fi1 and fi2 were created // by this package's Stat/Lstat method. diff --git a/lib/fs/casefs.go b/lib/fs/casefs.go index 6a27be845..368a5e4df 100644 --- a/lib/fs/casefs.go +++ b/lib/fs/casefs.go @@ -55,22 +55,22 @@ type caseFilesystemRegistry struct { startCleaner sync.Once } -func newFSKey(fs Filesystem, opts ...Option) fskey { +func newFSKey(fs Filesystem) fskey { k := fskey{ fstype: fs.Type(), uri: fs.URI(), } - if len(opts) > 0 { - k.opts = opts[0].id + if opts := fs.Options(); len(opts) > 0 { + k.opts = opts[0].String() for _, o := range opts[1:] { - k.opts += "&" + o.id + k.opts += "&" + o.String() } } return k } -func (r *caseFilesystemRegistry) get(fs Filesystem, opts ...Option) Filesystem { - k := newFSKey(fs, opts...) +func (r *caseFilesystemRegistry) get(fs Filesystem) Filesystem { + k := newFSKey(fs) // Use double locking when getting a caseFs. In the common case it will // already exist and we take the read lock fast path. If it doesn't, we @@ -136,10 +136,8 @@ type caseFilesystem struct { // from the real path. It is safe to use with any filesystem, i.e. also a // case-sensitive one. However it will add some overhead and thus shouldn't be // used if the filesystem is known to already behave case-sensitively. -func NewCaseFilesystem(fs Filesystem, opts ...Option) Filesystem { - return wrapFilesystem(fs, func(fs Filesystem) Filesystem { - return globalCaseFilesystemRegistry.get(fs, opts...) - }) +func NewCaseFilesystem(fs Filesystem) Filesystem { + return wrapFilesystem(fs, globalCaseFilesystemRegistry.get) } func (f *caseFilesystem) Chmod(name string, mode FileMode) error { diff --git a/lib/fs/errorfs.go b/lib/fs/errorfs.go index cf0e61cc9..d1ed43867 100644 --- a/lib/fs/errorfs.go +++ b/lib/fs/errorfs.go @@ -45,7 +45,10 @@ func (fs *errorFilesystem) Roots() ([]string, error) { retur func (fs *errorFilesystem) Usage(name string) (Usage, error) { return Usage{}, fs.err } func (fs *errorFilesystem) Type() FilesystemType { return fs.fsType } func (fs *errorFilesystem) URI() string { return fs.uri } -func (fs *errorFilesystem) SameFile(fi1, fi2 FileInfo) bool { return false } +func (fs *errorFilesystem) Options() []Option { + return nil +} +func (fs *errorFilesystem) SameFile(fi1, fi2 FileInfo) bool { return false } func (fs *errorFilesystem) Watch(path string, ignore Matcher, ctx context.Context, ignorePerms bool) (<-chan Event, <-chan error, error) { return nil, nil, fs.err } diff --git a/lib/fs/fakefs.go b/lib/fs/fakefs.go index d421b46ff..d8edc15cd 100644 --- a/lib/fs/fakefs.go +++ b/lib/fs/fakefs.go @@ -640,6 +640,10 @@ func (fs *fakefs) URI() string { return fs.uri } +func (fs *fakefs) Options() []Option { + return nil +} + func (fs *fakefs) SameFile(fi1, fi2 FileInfo) bool { // BUG: real systems base file sameness on path, inodes, etc // we try our best, but FileInfo just doesn't have enough data diff --git a/lib/fs/filesystem.go b/lib/fs/filesystem.go index 7580d425e..fb8d51d70 100644 --- a/lib/fs/filesystem.go +++ b/lib/fs/filesystem.go @@ -47,6 +47,7 @@ type Filesystem interface { Usage(name string) (Usage, error) Type() FilesystemType URI() string + Options() []Option SameFile(fi1, fi2 FileInfo) bool } @@ -178,9 +179,15 @@ var IsPermission = os.IsPermission // IsPathSeparator is the equivalent of os.IsPathSeparator var IsPathSeparator = os.IsPathSeparator -type Option struct { - apply func(Filesystem) - id string +// Option modifies a filesystem at creation. An option might be specific +// to a filesystem-type. +// +// String is used to detect options with the same effect, i.e. must be different +// for options with different effects. Meaning if an option has parameters, a +// representation of those must be part of the returned string. +type Option interface { + String() string + apply(Filesystem) } func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem { diff --git a/lib/fs/walkfs.go b/lib/fs/walkfs.go index d883a7e50..27cdb9667 100644 --- a/lib/fs/walkfs.go +++ b/lib/fs/walkfs.go @@ -63,10 +63,20 @@ type WalkFunc func(path string, info FileInfo, err error) error type walkFilesystem struct { Filesystem + checkInfiniteRecursion bool } func NewWalkFilesystem(next Filesystem) Filesystem { - return &walkFilesystem{next} + fs := &walkFilesystem{ + Filesystem: next, + } + for _, opt := range next.Options() { + if _, ok := opt.(*OptionJunctionsAsDirs); ok { + fs.checkInfiniteRecursion = true + break + } + } + return fs } // walk recursively descends path, calling walkFn. @@ -89,11 +99,13 @@ func (f *walkFilesystem) walk(path string, info FileInfo, walkFn WalkFunc, ances return nil } - if !ancestors.Contains(info) { - ancestors.Push(info) - defer ancestors.Pop() - } else { - return walkFn(path, info, ErrInfiniteRecursion) + if f.checkInfiniteRecursion { + if !ancestors.Contains(info) { + ancestors.Push(info) + defer ancestors.Pop() + } else { + return walkFn(path, info, ErrInfiniteRecursion) + } } names, err := f.DirNames(path) @@ -131,6 +143,9 @@ func (f *walkFilesystem) Walk(root string, walkFn WalkFunc) error { if err != nil { return walkFn(root, nil, err) } - ancestors := &ancestorDirList{fs: f.Filesystem} + var ancestors *ancestorDirList + if f.checkInfiniteRecursion { + ancestors = &ancestorDirList{fs: f.Filesystem} + } return f.walk(root, info, walkFn, ancestors) } diff --git a/lib/fs/walkfs_test.go b/lib/fs/walkfs_test.go index 3dd44cb8d..cf4c71f84 100644 --- a/lib/fs/walkfs_test.go +++ b/lib/fs/walkfs_test.go @@ -57,7 +57,7 @@ func testWalkTraverseDirJunct(t *testing.T, fsType FilesystemType, uri string) { t.Skip("Directory junctions are available and tested on windows only") } - fs := NewFilesystem(fsType, uri, WithJunctionsAsDirs()) + fs := NewFilesystem(fsType, uri, new(OptionJunctionsAsDirs)) if err := fs.MkdirAll("target/foo", 0); err != nil { t.Fatal(err) @@ -90,7 +90,7 @@ func testWalkInfiniteRecursion(t *testing.T, fsType FilesystemType, uri string) t.Skip("Infinite recursion detection is tested on windows only") } - fs := NewFilesystem(fsType, uri, WithJunctionsAsDirs()) + fs := NewFilesystem(fsType, uri, new(OptionJunctionsAsDirs)) if err := fs.MkdirAll("target/foo", 0); err != nil { t.Fatal(err) diff --git a/lib/scanner/virtualfs_test.go b/lib/scanner/virtualfs_test.go index 0a33d3101..7179f5b53 100644 --- a/lib/scanner/virtualfs_test.go +++ b/lib/scanner/virtualfs_test.go @@ -97,6 +97,10 @@ func (s singleFileFS) Open(name string) (fs.File, error) { return &fakeFile{s.name, s.filesize, 0}, nil } +func (s singleFileFS) Options() []fs.Option { + return nil +} + type fakeInfo struct { name string size int64