Browse Source

lib: Get rid of buggy filesystem wrapping (#8257)

Simon Frei 3 years ago
parent
commit
db72579f0e

+ 2 - 2
lib/config/config_test.go

@@ -457,7 +457,7 @@ func TestIssue1262(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	actual := cfg.Folders()["test"].Filesystem().URI()
+	actual := cfg.Folders()["test"].Filesystem(nil).URI()
 	expected := `e:\`
 
 	if actual != expected {
@@ -494,7 +494,7 @@ func TestFolderPath(t *testing.T) {
 		Path: "~/tmp",
 	}
 
-	realPath := folder.Filesystem().URI()
+	realPath := folder.Filesystem(nil).URI()
 	if !filepath.IsAbs(realPath) {
 		t.Error(realPath, "should be absolute")
 	}

+ 18 - 12
lib/config/folderconfiguration.go

@@ -16,6 +16,7 @@ import (
 
 	"github.com/shirou/gopsutil/v3/disk"
 
+	"github.com/syncthing/syncthing/lib/db"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/util"
@@ -42,24 +43,29 @@ func (f FolderConfiguration) Copy() FolderConfiguration {
 	return c
 }
 
-func (f FolderConfiguration) Filesystem() fs.Filesystem {
+// Filesystem creates a filesystem for the path and options of this folder.
+// The fset parameter may be nil, in which case no mtime handling on top of
+// the fileystem is provided.
+func (f FolderConfiguration) Filesystem(fset *db.FileSet) fs.Filesystem {
 	// This is intentionally not a pointer method, because things like
-	// cfg.Folders["default"].Filesystem() should be valid.
-	var opts []fs.Option
+	// cfg.Folders["default"].Filesystem(nil) should be valid.
+	opts := make([]fs.Option, 0, 3)
 	if f.FilesystemType == fs.FilesystemTypeBasic && f.JunctionsAsDirs {
 		opts = append(opts, new(fs.OptionJunctionsAsDirs))
 	}
-	filesystem := fs.NewFilesystem(f.FilesystemType, f.Path, opts...)
 	if !f.CaseSensitiveFS {
-		filesystem = fs.NewCaseFilesystem(filesystem)
+		opts = append(opts, new(fs.OptionDetectCaseConflicts))
 	}
-	return filesystem
+	if fset != nil {
+		opts = append(opts, fset.MtimeOption())
+	}
+	return fs.NewFilesystem(f.FilesystemType, f.Path, opts...)
 }
 
 func (f FolderConfiguration) ModTimeWindow() time.Duration {
 	dur := time.Duration(f.RawModTimeWindowS) * time.Second
 	if f.RawModTimeWindowS < 1 && runtime.GOOS == "android" {
-		if usage, err := disk.Usage(f.Filesystem().URI()); err != nil {
+		if usage, err := disk.Usage(f.Filesystem(nil).URI()); err != nil {
 			dur = 2 * time.Second
 			l.Debugf(`Detecting FS at "%v" on android: Setting mtime window to 2s: err == "%v"`, f.Path, err)
 		} else if strings.HasPrefix(strings.ToLower(usage.Fstype), "ext2") || strings.HasPrefix(strings.ToLower(usage.Fstype), "ext3") || strings.HasPrefix(strings.ToLower(usage.Fstype), "ext4") {
@@ -89,7 +95,7 @@ func (f *FolderConfiguration) CreateMarker() error {
 		// begin with.
 		permBits = 0700
 	}
-	fs := f.Filesystem()
+	fs := f.Filesystem(nil)
 	err := fs.Mkdir(DefaultMarkerName, permBits)
 	if err != nil {
 		return err
@@ -106,7 +112,7 @@ func (f *FolderConfiguration) CreateMarker() error {
 
 // CheckPath returns nil if the folder root exists and contains the marker file
 func (f *FolderConfiguration) CheckPath() error {
-	fi, err := f.Filesystem().Stat(".")
+	fi, err := f.Filesystem(nil).Stat(".")
 	if err != nil {
 		if !fs.IsNotExist(err) {
 			return err
@@ -124,7 +130,7 @@ func (f *FolderConfiguration) CheckPath() error {
 		return ErrPathNotDirectory
 	}
 
-	_, err = f.Filesystem().Stat(f.MarkerName)
+	_, err = f.Filesystem(nil).Stat(f.MarkerName)
 	if err != nil {
 		if !fs.IsNotExist(err) {
 			return err
@@ -145,7 +151,7 @@ func (f *FolderConfiguration) CreateRoot() (err error) {
 		permBits = 0700
 	}
 
-	filesystem := f.Filesystem()
+	filesystem := f.Filesystem(nil)
 
 	if _, err = filesystem.Stat("."); fs.IsNotExist(err) {
 		err = filesystem.MkdirAll(".", permBits)
@@ -256,7 +262,7 @@ func (f *FolderConfiguration) CheckAvailableSpace(req uint64) error {
 	if val <= 0 {
 		return nil
 	}
-	fs := f.Filesystem()
+	fs := f.Filesystem(nil)
 	usage, err := fs.Usage(".")
 	if err != nil {
 		return nil

+ 4 - 4
lib/config/migrations.go

@@ -199,7 +199,7 @@ func migrateToConfigV23(cfg *Configuration) {
 	// marker name in later versions.
 
 	for i := range cfg.Folders {
-		fs := cfg.Folders[i].Filesystem()
+		fs := cfg.Folders[i].Filesystem(nil)
 		// Invalid config posted, or tests.
 		if fs == nil {
 			continue
@@ -235,18 +235,18 @@ func migrateToConfigV21(cfg *Configuration) {
 		switch folder.Versioning.Type {
 		case "simple", "trashcan":
 			// Clean out symlinks in the known place
-			cleanSymlinks(folder.Filesystem(), ".stversions")
+			cleanSymlinks(folder.Filesystem(nil), ".stversions")
 		case "staggered":
 			versionDir := folder.Versioning.Params["versionsPath"]
 			if versionDir == "" {
 				// default place
-				cleanSymlinks(folder.Filesystem(), ".stversions")
+				cleanSymlinks(folder.Filesystem(nil), ".stversions")
 			} else if filepath.IsAbs(versionDir) {
 				// absolute
 				cleanSymlinks(fs.NewFilesystem(fs.FilesystemTypeBasic, versionDir), ".")
 			} else {
 				// relative to folder
-				cleanSymlinks(folder.Filesystem(), versionDir)
+				cleanSymlinks(folder.Filesystem(nil), versionDir)
 			}
 		}
 	}

+ 3 - 3
lib/db/set.go

@@ -403,8 +403,8 @@ func (s *FileSet) SetIndexID(device protocol.DeviceID, id protocol.IndexID) {
 	}
 }
 
-func (s *FileSet) MtimeFS(filesystem fs.Filesystem) fs.Filesystem {
-	opStr := fmt.Sprintf("%s MtimeFS()", s.folder)
+func (s *FileSet) MtimeOption() fs.Option {
+	opStr := fmt.Sprintf("%s MtimeOption()", s.folder)
 	l.Debugf(opStr)
 	prefix, err := s.db.keyer.GenerateMtimesKey(nil, []byte(s.folder))
 	if backend.IsClosed(err) {
@@ -413,7 +413,7 @@ func (s *FileSet) MtimeFS(filesystem fs.Filesystem) fs.Filesystem {
 		fatalError(err, opStr, s.db)
 	}
 	kv := NewNamespacedKV(s.db, string(prefix))
-	return fs.NewMtimeFS(filesystem, kv)
+	return fs.NewMtimeOption(kv)
 }
 
 func (s *FileSet) ListDevices() []protocol.DeviceID {

+ 2 - 1
lib/fs/basicfs.go

@@ -27,12 +27,13 @@ var (
 
 type OptionJunctionsAsDirs struct{}
 
-func (o *OptionJunctionsAsDirs) apply(fs Filesystem) {
+func (o *OptionJunctionsAsDirs) apply(fs Filesystem) Filesystem {
 	if basic, ok := fs.(*BasicFilesystem); !ok {
 		l.Warnln("WithJunctionsAsDirs must only be used with FilesystemTypeBasic")
 	} else {
 		basic.junctionsAsDirs = true
 	}
+	return fs
 }
 
 func (o *OptionJunctionsAsDirs) String() string {

+ 16 - 10
lib/fs/casefs.go

@@ -123,21 +123,27 @@ func (r *caseFilesystemRegistry) cleaner() {
 
 var globalCaseFilesystemRegistry = caseFilesystemRegistry{fss: make(map[fskey]*caseFilesystem)}
 
-// caseFilesystem is a BasicFilesystem with additional checks to make a
-// potentially case insensitive underlying FS behave like it's case-sensitive.
-type caseFilesystem struct {
-	Filesystem
-	realCaser
-}
-
-// NewCaseFilesystem ensures that the given, potentially case-insensitive filesystem
+// OptionDetectCaseConflicts ensures that the potentially case-insensitive filesystem
 // behaves like a case-sensitive filesystem. Meaning that it takes into account
 // the real casing of a path and returns ErrCaseConflict if the given path differs
 // 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) Filesystem {
-	return wrapFilesystem(fs, globalCaseFilesystemRegistry.get)
+type OptionDetectCaseConflicts struct{}
+
+func (o *OptionDetectCaseConflicts) apply(fs Filesystem) Filesystem {
+	return globalCaseFilesystemRegistry.get(fs)
+}
+
+func (o *OptionDetectCaseConflicts) String() string {
+	return "detectCaseConflicts"
+}
+
+// caseFilesystem is a BasicFilesystem with additional checks to make a
+// potentially case insensitive underlying FS behave like it's case-sensitive.
+type caseFilesystem struct {
+	Filesystem
+	realCaser
 }
 
 func (f *caseFilesystem) Chmod(name string, mode FileMode) error {

+ 7 - 3
lib/fs/casefs_test.go

@@ -34,8 +34,12 @@ func TestRealCase(t *testing.T) {
 	})
 }
 
+func newCaseFilesystem(fsys Filesystem) *caseFilesystem {
+	return globalCaseFilesystemRegistry.get(fsys).(*caseFilesystem)
+}
+
 func testRealCase(t *testing.T, fsys Filesystem) {
-	testFs := NewCaseFilesystem(fsys).(*caseFilesystem)
+	testFs := newCaseFilesystem(fsys)
 	comps := []string{"Foo", "bar", "BAZ", "bAs"}
 	path := filepath.Join(comps...)
 	testFs.MkdirAll(filepath.Join(comps[:len(comps)-1]...), 0777)
@@ -86,7 +90,7 @@ func TestRealCaseSensitive(t *testing.T) {
 }
 
 func testRealCaseSensitive(t *testing.T, fsys Filesystem) {
-	testFs := NewCaseFilesystem(fsys).(*caseFilesystem)
+	testFs := newCaseFilesystem(fsys)
 
 	names := make([]string, 2)
 	names[0] = "foo"
@@ -139,7 +143,7 @@ func testCaseFSStat(t *testing.T, fsys Filesystem) {
 		sensitive = false
 	}
 
-	testFs := NewCaseFilesystem(fsys)
+	testFs := newCaseFilesystem(fsys)
 	_, err = testFs.Stat("FOO")
 	if sensitive {
 		if IsNotExist(err) {

+ 31 - 16
lib/fs/filesystem.go

@@ -202,10 +202,29 @@ var IsPathSeparator = os.IsPathSeparator
 // representation of those must be part of the returned string.
 type Option interface {
 	String() string
-	apply(Filesystem)
+	apply(Filesystem) Filesystem
 }
 
 func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem {
+	var caseOpt Option
+	var mtimeOpt Option
+	i := 0
+	for _, opt := range opts {
+		if caseOpt != nil && mtimeOpt != nil {
+			break
+		}
+		switch opt.(type) {
+		case *OptionDetectCaseConflicts:
+			caseOpt = opt
+		case *optionMtime:
+			mtimeOpt = opt
+		default:
+			opts[i] = opt
+			i++
+		}
+	}
+	opts = opts[:i]
+
 	var fs Filesystem
 	switch fsType {
 	case FilesystemTypeBasic:
@@ -221,6 +240,17 @@ func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem
 		}
 	}
 
+	// Case handling is the innermost, as any filesystem calls by wrappers should be case-resolved
+	if caseOpt != nil {
+		fs = caseOpt.apply(fs)
+	}
+
+	// mtime handling should happen inside walking, as filesystem calls while
+	// walking should be mtime-resolved too
+	if mtimeOpt != nil {
+		fs = mtimeOpt.apply(fs)
+	}
+
 	if l.ShouldDebug("walkfs") {
 		return NewWalkFilesystem(&logFilesystem{fs})
 	}
@@ -289,21 +319,6 @@ func Canonicalize(file string) (string, error) {
 	return file, nil
 }
 
-// wrapFilesystem should always be used when wrapping a Filesystem.
-// It ensures proper wrapping order, which right now means:
-// `logFilesystem` needs to be the outermost wrapper for caller lookup.
-func wrapFilesystem(fs Filesystem, wrapFn func(Filesystem) Filesystem) Filesystem {
-	logFs, ok := fs.(*logFilesystem)
-	if ok {
-		fs = logFs.Filesystem
-	}
-	fs = wrapFn(fs)
-	if ok {
-		fs = &logFilesystem{fs}
-	}
-	return fs
-}
-
 // unwrapFilesystem removes "wrapping" filesystems to expose the filesystem of the requested wrapperType, if it exists.
 func unwrapFilesystem(fs Filesystem, wrapperType filesystemWrapperType) (Filesystem, bool) {
 	var ok bool

+ 28 - 33
lib/fs/mtimefs.go

@@ -33,20 +33,34 @@ func WithCaseInsensitivity(v bool) MtimeFSOption {
 	}
 }
 
-// NewMtimeFS returns a filesystem with nanosecond mtime precision, regardless
-// of what shenanigans the underlying filesystem gets up to.
-func NewMtimeFS(fs Filesystem, db database, options ...MtimeFSOption) Filesystem {
-	return wrapFilesystem(fs, func(underlying Filesystem) Filesystem {
-		f := &mtimeFS{
-			Filesystem: underlying,
-			chtimes:    underlying.Chtimes, // for mocking it out in the tests
-			db:         db,
-		}
-		for _, opt := range options {
-			opt(f)
-		}
-		return f
-	})
+type optionMtime struct {
+	db      database
+	options []MtimeFSOption
+}
+
+// NewMtimeOption makes any filesystem provide nanosecond mtime precision,
+// regardless of what shenanigans the underlying filesystem gets up to.
+func NewMtimeOption(db database, options ...MtimeFSOption) Option {
+	return &optionMtime{
+		db:      db,
+		options: options,
+	}
+}
+
+func (o *optionMtime) apply(fs Filesystem) Filesystem {
+	f := &mtimeFS{
+		Filesystem: fs,
+		chtimes:    fs.Chtimes, // for mocking it out in the tests
+		db:         o.db,
+	}
+	for _, opt := range o.options {
+		opt(f)
+	}
+	return f
+}
+
+func (_ *optionMtime) String() string {
+	return "mtime"
 }
 
 func (f *mtimeFS) Chtimes(name string, atime, mtime time.Time) error {
@@ -104,25 +118,6 @@ func (f *mtimeFS) Lstat(name string) (FileInfo, error) {
 	return info, nil
 }
 
-func (f *mtimeFS) Walk(root string, walkFn WalkFunc) error {
-	return f.Filesystem.Walk(root, func(path string, info FileInfo, err error) error {
-		if info != nil {
-			mtimeMapping, loadErr := f.load(path)
-			if loadErr != nil && err == nil {
-				// The iterator gets to deal with the error
-				err = loadErr
-			}
-			if mtimeMapping.Real == info.ModTime() {
-				info = mtimeFileInfo{
-					FileInfo: info,
-					mtime:    mtimeMapping.Virtual,
-				}
-			}
-		}
-		return walkFn(path, info, err)
-	})
-}
-
 func (f *mtimeFS) Create(name string) (File, error) {
 	fd, err := f.Filesystem.Create(name)
 	if err != nil {

+ 16 - 10
lib/fs/mtimefs_test.go

@@ -26,7 +26,7 @@ func TestMtimeFS(t *testing.T) {
 	// a random time with nanosecond precision
 	testTime := time.Unix(1234567890, 123456789)
 
-	mtimefs := newMtimeFS(newBasicFilesystem("."), make(mapStore))
+	mtimefs := newMtimeFS(".", make(mapStore))
 
 	// Do one Chtimes call that will go through to the normal filesystem
 	mtimefs.chtimes = os.Chtimes
@@ -88,8 +88,8 @@ func TestMtimeFSWalk(t *testing.T) {
 	}
 	defer func() { _ = os.RemoveAll(dir) }()
 
-	underlying := NewFilesystem(FilesystemTypeBasic, dir)
-	mtimefs := newMtimeFS(underlying, make(mapStore))
+	mtimefs, walkFs := newMtimeFSWithWalk(dir, make(mapStore))
+	underlying := mtimefs.Filesystem
 	mtimefs.chtimes = failChtimes
 
 	if err := os.WriteFile(filepath.Join(dir, "file"), []byte("hello"), 0644); err != nil {
@@ -120,7 +120,7 @@ func TestMtimeFSWalk(t *testing.T) {
 	}
 
 	found := false
-	_ = mtimefs.Walk("", func(path string, info FileInfo, err error) error {
+	_ = walkFs.Walk("", func(path string, info FileInfo, err error) error {
 		if path == "file" {
 			found = true
 			if !info.ModTime().Equal(newTime) {
@@ -142,8 +142,8 @@ func TestMtimeFSOpen(t *testing.T) {
 	}
 	defer func() { _ = os.RemoveAll(dir) }()
 
-	underlying := NewFilesystem(FilesystemTypeBasic, dir)
-	mtimefs := newMtimeFS(underlying, make(mapStore))
+	mtimefs := newMtimeFS(dir, make(mapStore))
+	underlying := mtimefs.Filesystem
 	mtimefs.chtimes = failChtimes
 
 	if err := os.WriteFile(filepath.Join(dir, "file"), []byte("hello"), 0644); err != nil {
@@ -222,12 +222,12 @@ func TestMtimeFSInsensitive(t *testing.T) {
 
 	// The test should fail with a case sensitive mtimefs
 	t.Run("with case sensitive mtimefs", func(t *testing.T) {
-		theTest(t, newMtimeFS(newBasicFilesystem("."), make(mapStore)), false)
+		theTest(t, newMtimeFS(".", make(mapStore)), false)
 	})
 
 	// And succeed with a case insensitive one.
 	t.Run("with case insensitive mtimefs", func(t *testing.T) {
-		theTest(t, newMtimeFS(newBasicFilesystem("."), make(mapStore), WithCaseInsensitivity(true)), true)
+		theTest(t, newMtimeFS(".", make(mapStore), WithCaseInsensitivity(true)), true)
 	})
 }
 
@@ -261,6 +261,12 @@ func evilChtimes(name string, mtime, atime time.Time) error {
 	return os.Chtimes(name, mtime.Add(300*time.Hour).Truncate(time.Hour), atime.Add(300*time.Hour).Truncate(time.Hour))
 }
 
-func newMtimeFS(fs Filesystem, db database, options ...MtimeFSOption) *mtimeFS {
-	return NewMtimeFS(fs, db, options...).(*mtimeFS)
+func newMtimeFS(path string, db database, options ...MtimeFSOption) *mtimeFS {
+	mtimefs, _ := newMtimeFSWithWalk(path, db, options...)
+	return mtimefs
+}
+
+func newMtimeFSWithWalk(path string, db database, options ...MtimeFSOption) (*mtimeFS, *walkFilesystem) {
+	wfs := NewFilesystem(FilesystemTypeBasic, path, NewMtimeOption(db, options...)).(*walkFilesystem)
+	return wfs.Filesystem.(*mtimeFS), wfs
 }

+ 2 - 2
lib/model/folder.go

@@ -108,7 +108,7 @@ func newFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg conf
 		shortID:       model.shortID,
 		fset:          fset,
 		ignores:       ignores,
-		mtimefs:       fset.MtimeFS(cfg.Filesystem()),
+		mtimefs:       cfg.Filesystem(fset),
 		modTimeWindow: cfg.ModTimeWindow(),
 		done:          make(chan struct{}),
 
@@ -1001,7 +1001,7 @@ func (f *folder) monitorWatch(ctx context.Context) {
 	for {
 		select {
 		case <-failTimer.C:
-			eventChan, errChan, err = f.Filesystem().Watch(".", f.ignores, ctx, f.IgnorePerms)
+			eventChan, errChan, err = f.mtimefs.Watch(".", f.ignores, ctx, f.IgnorePerms)
 			// We do this once per minute initially increased to
 			// max one hour in case of repeat failures.
 			f.scanOnWatchErr()

+ 6 - 6
lib/model/folder_recvonly_test.go

@@ -28,7 +28,7 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 	defer cleanupModel(m)
 	addFakeConn(m, device1, f.ID)
 
@@ -110,7 +110,7 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 	defer cleanupModel(m)
 	addFakeConn(m, device1, f.ID)
 
@@ -200,7 +200,7 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 	defer cleanupModel(m)
 	addFakeConn(m, device1, f.ID)
 
@@ -270,7 +270,7 @@ func TestRecvOnlyDeletedRemoteDrop(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 	defer cleanupModel(m)
 	addFakeConn(m, device1, f.ID)
 
@@ -335,7 +335,7 @@ func TestRecvOnlyRemoteUndoChanges(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 	defer cleanupModel(m)
 	addFakeConn(m, device1, f.ID)
 
@@ -425,7 +425,7 @@ func TestRecvOnlyRevertOwnID(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 	defer cleanupModel(m)
 	addFakeConn(m, device1, f.ID)
 

+ 1 - 1
lib/model/folder_sendrecv.go

@@ -1264,7 +1264,7 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
 	// Hope that it's usually in the same folder, so start with that one.
 	folders := []string{f.folderID}
 	for folder, cfg := range f.model.cfg.Folders() {
-		folderFilesystems[folder] = cfg.Filesystem()
+		folderFilesystems[folder] = cfg.Filesystem(nil)
 		if folder != f.folderID {
 			folders = append(folders, folder)
 		}

+ 14 - 14
lib/model/folder_sendrecv_test.go

@@ -109,7 +109,7 @@ func setupSendReceiveFolder(t testing.TB, files ...protocol.FileInfo) (*testMode
 func cleanupSRFolder(f *sendReceiveFolder, m *testModel, wrapperCancel context.CancelFunc) {
 	wrapperCancel()
 	os.Remove(m.cfg.ConfigPath())
-	os.RemoveAll(f.Filesystem().URI())
+	os.RemoveAll(f.Filesystem(nil).URI())
 }
 
 // Layout of the files: (indexes from the above array)
@@ -172,7 +172,7 @@ func TestHandleFileWithTemp(t *testing.T) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t, existingFile)
 	defer cleanupSRFolder(f, m, wcfgCancel)
 
-	if _, err := prepareTmpFile(f.Filesystem()); err != nil {
+	if _, err := prepareTmpFile(f.Filesystem(nil)); err != nil {
 		t.Fatal(err)
 	}
 
@@ -230,7 +230,7 @@ func TestCopierFinder(t *testing.T) {
 
 			defer cleanupSRFolder(f, m, wcfgCancel)
 
-			if _, err := prepareTmpFile(f.Filesystem()); err != nil {
+			if _, err := prepareTmpFile(f.Filesystem(nil)); err != nil {
 				t.Fatal(err)
 			}
 
@@ -290,7 +290,7 @@ func TestCopierFinder(t *testing.T) {
 			}
 
 			// Verify that the fetched blocks have actually been written to the temp file
-			blks, err := scanner.HashFile(context.TODO(), f.Filesystem(), tempFile, protocol.MinBlockSize, nil, false)
+			blks, err := scanner.HashFile(context.TODO(), f.Filesystem(nil), tempFile, protocol.MinBlockSize, nil, false)
 			if err != nil {
 				t.Log(err)
 			}
@@ -308,7 +308,7 @@ func TestWeakHash(t *testing.T) {
 	// Setup the model/pull environment
 	model, fo, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(fo, model, wcfgCancel)
-	ffs := fo.Filesystem()
+	ffs := fo.Filesystem(nil)
 
 	tempFile := fs.TempName("weakhash")
 	var shift int64 = 10
@@ -673,7 +673,7 @@ func TestDeregisterOnFailInPull(t *testing.T) {
 func TestIssue3164(t *testing.T) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m, wcfgCancel)
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 	tmpDir := ffs.URI()
 
 	ignDir := filepath.Join("issue3164", "oktodelete")
@@ -764,7 +764,7 @@ func TestDiffEmpty(t *testing.T) {
 func TestDeleteIgnorePerms(t *testing.T) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m, wcfgCancel)
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 	f.IgnorePerms = true
 
 	name := "deleteIgnorePerms"
@@ -806,7 +806,7 @@ func TestCopyOwner(t *testing.T) {
 	f.folder.FolderConfiguration.CopyOwnershipFromParent = true
 
 	f.fset = newFileSet(t, f.ID, m.db)
-	f.mtimefs = f.fset.MtimeFS(f.Filesystem())
+	f.mtimefs = f.Filesystem(f.fset)
 
 	// Create a parent dir with a certain owner/group.
 
@@ -905,7 +905,7 @@ func TestCopyOwner(t *testing.T) {
 func TestSRConflictReplaceFileByDir(t *testing.T) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m, wcfgCancel)
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 
 	name := "foo"
 
@@ -937,7 +937,7 @@ func TestSRConflictReplaceFileByDir(t *testing.T) {
 func TestSRConflictReplaceFileByLink(t *testing.T) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m, wcfgCancel)
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 
 	name := "foo"
 
@@ -970,7 +970,7 @@ func TestSRConflictReplaceFileByLink(t *testing.T) {
 func TestDeleteBehindSymlink(t *testing.T) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m, wcfgCancel)
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 
 	destDir := createTmpDir()
 	defer os.RemoveAll(destDir)
@@ -1063,7 +1063,7 @@ func TestPullCtxCancel(t *testing.T) {
 func TestPullDeleteUnscannedDir(t *testing.T) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m, wcfgCancel)
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 
 	dir := "foobar"
 	must(t, ffs.MkdirAll(dir, 0777))
@@ -1092,7 +1092,7 @@ func TestPullDeleteUnscannedDir(t *testing.T) {
 func TestPullCaseOnlyPerformFinish(t *testing.T) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m, wcfgCancel)
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 
 	name := "foo"
 	contents := []byte("contents")
@@ -1153,7 +1153,7 @@ func TestPullCaseOnlySymlink(t *testing.T) {
 func testPullCaseOnlyDirOrSymlink(t *testing.T, dir bool) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m, wcfgCancel)
-	ffs := f.Filesystem()
+	ffs := f.Filesystem(nil)
 
 	name := "foo"
 	if dir {

+ 10 - 10
lib/model/model.go

@@ -332,7 +332,7 @@ func (m *model) StartDeadlockDetector(timeout time.Duration) {
 
 // Need to hold lock on m.fmut when calling this.
 func (m *model) addAndStartFolderLocked(cfg config.FolderConfiguration, fset *db.FileSet, cacheIgnoredFiles bool) {
-	ignores := ignore.New(cfg.Filesystem(), ignore.WithCache(cacheIgnoredFiles))
+	ignores := ignore.New(cfg.Filesystem(nil), ignore.WithCache(cacheIgnoredFiles))
 	if cfg.Type != config.FolderTypeReceiveEncrypted {
 		if err := ignores.Load(".stignore"); err != nil && !fs.IsNotExist(err) {
 			l.Warnln("Loading ignores:", err)
@@ -396,7 +396,7 @@ func (m *model) addAndStartFolderLockedWithIgnores(cfg config.FolderConfiguratio
 	}
 
 	// These are our metadata files, and they should always be hidden.
-	ffs := cfg.Filesystem()
+	ffs := cfg.Filesystem(nil)
 	_ = ffs.Hide(config.DefaultMarkerName)
 	_ = ffs.Hide(".stversions")
 	_ = ffs.Hide(".stignore")
@@ -428,7 +428,7 @@ func (m *model) warnAboutOverwritingProtectedFiles(cfg config.FolderConfiguratio
 	}
 
 	// This is a bit of a hack.
-	ffs := cfg.Filesystem()
+	ffs := cfg.Filesystem(nil)
 	if ffs.Type() != fs.FilesystemTypeBasic {
 		return
 	}
@@ -478,7 +478,7 @@ func (m *model) removeFolder(cfg config.FolderConfiguration) {
 	if isPathUnique {
 		// Remove (if empty and removable) or move away (if non-empty or
 		// otherwise not removable) Syncthing-specific marker files.
-		fs := cfg.Filesystem()
+		fs := cfg.Filesystem(nil)
 		if err := fs.Remove(config.DefaultMarkerName); err != nil {
 			moved := config.DefaultMarkerName + time.Now().Format(".removed-20060102-150405")
 			_ = fs.Rename(config.DefaultMarkerName, moved)
@@ -1839,7 +1839,7 @@ func (m *model) Request(deviceID protocol.DeviceID, folder, name string, blockNo
 	// Grab the FS after limiting, as it causes I/O and we want to minimize
 	// the race time between the symlink check and the read.
 
-	folderFs := folderCfg.Filesystem()
+	folderFs := folderCfg.Filesystem(nil)
 
 	if err := osutil.TraversesSymlink(folderFs, filepath.Dir(name)); err != nil {
 		l.Debugf("%v REQ(in) traversal check: %s - %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size)
@@ -2005,7 +2005,7 @@ func (m *model) GetMtimeMapping(folder string, file string) (fs.MtimeMapping, er
 	if !ok {
 		return fs.MtimeMapping{}, ErrFolderMissing
 	}
-	return fs.GetMtimeMapping(ffs.MtimeFS(fcfg.Filesystem()), file)
+	return fs.GetMtimeMapping(fcfg.Filesystem(ffs), file)
 }
 
 // Connection returns the current connection for device, and a boolean whether a connection was found.
@@ -2039,7 +2039,7 @@ func (m *model) LoadIgnores(folder string) ([]string, []string, error) {
 	}
 
 	if !ignoresOk {
-		ignores = ignore.New(cfg.Filesystem())
+		ignores = ignore.New(cfg.Filesystem(nil))
 	}
 
 	err := ignores.Load(".stignore")
@@ -2094,7 +2094,7 @@ func (m *model) setIgnores(cfg config.FolderConfiguration, content []string) err
 		return err
 	}
 
-	if err := ignore.WriteIgnores(cfg.Filesystem(), ".stignore", content); err != nil {
+	if err := ignore.WriteIgnores(cfg.Filesystem(nil), ".stignore", content); err != nil {
 		l.Warnln("Saving .stignore:", err)
 		return err
 	}
@@ -3207,7 +3207,7 @@ type storedEncryptionToken struct {
 }
 
 func readEncryptionToken(cfg config.FolderConfiguration) ([]byte, error) {
-	fd, err := cfg.Filesystem().Open(encryptionTokenPath(cfg))
+	fd, err := cfg.Filesystem(nil).Open(encryptionTokenPath(cfg))
 	if err != nil {
 		return nil, err
 	}
@@ -3221,7 +3221,7 @@ func readEncryptionToken(cfg config.FolderConfiguration) ([]byte, error) {
 
 func writeEncryptionToken(token []byte, cfg config.FolderConfiguration) error {
 	tokenName := encryptionTokenPath(cfg)
-	fd, err := cfg.Filesystem().OpenFile(tokenName, fs.OptReadWrite|fs.OptCreate, 0666)
+	fd, err := cfg.Filesystem(nil).OpenFile(tokenName, fs.OptReadWrite|fs.OptCreate, 0666)
 	if err != nil {
 		return err
 	}

+ 28 - 28
lib/model/model_test.go

@@ -222,7 +222,7 @@ func BenchmarkIndex_100(b *testing.B) {
 func benchmarkIndex(b *testing.B, nfiles int) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(b)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	files := genFiles(nfiles)
 	must(b, m.Index(device1, fcfg.ID, files))
@@ -249,7 +249,7 @@ func BenchmarkIndexUpdate_10000_1(b *testing.B) {
 func benchmarkIndexUpdate(b *testing.B, nfiles, nufiles int) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(b)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	files := genFiles(nfiles)
 	ufiles := genFiles(nufiles)
@@ -1517,7 +1517,7 @@ func TestIgnores(t *testing.T) {
 
 	// Invalid path, treated like no patterns at all.
 	fcfg := config.FolderConfiguration{ID: "fresh", Path: "XXX"}
-	ignores := ignore.New(fcfg.Filesystem(), ignore.WithCache(m.cfg.Options().CacheIgnoredFiles))
+	ignores := ignore.New(fcfg.Filesystem(nil), ignore.WithCache(m.cfg.Options().CacheIgnoredFiles))
 	m.fmut.Lock()
 	m.folderCfgs[fcfg.ID] = fcfg
 	m.folderIgnores[fcfg.ID] = ignores
@@ -1709,7 +1709,7 @@ func TestRWScanRecovery(t *testing.T) {
 func TestGlobalDirectoryTree(t *testing.T) {
 	m, _, fcfg, wCancel := setupModelWithConnection(t)
 	defer wCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	b := func(isfile bool, path ...string) protocol.FileInfo {
 		typ := protocol.FileInfoTypeDirectory
@@ -2012,7 +2012,7 @@ func BenchmarkTree_100_10(b *testing.B) {
 func benchmarkTree(b *testing.B, n1, n2 int) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(b)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	m.ScanFolder(fcfg.ID)
 	files := genDeepFiles(n1, n2)
@@ -2477,7 +2477,7 @@ func TestIssue2571(t *testing.T) {
 
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
-	testFs := fcfg.Filesystem()
+	testFs := fcfg.Filesystem(nil)
 	defer os.RemoveAll(testFs.URI())
 
 	for _, dir := range []string{"toLink", "linkTarget"} {
@@ -2516,7 +2516,7 @@ func TestIssue4573(t *testing.T) {
 
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
-	testFs := fcfg.Filesystem()
+	testFs := fcfg.Filesystem(nil)
 	defer os.RemoveAll(testFs.URI())
 
 	must(t, testFs.MkdirAll("inaccessible", 0755))
@@ -2546,7 +2546,7 @@ func TestIssue4573(t *testing.T) {
 func TestInternalScan(t *testing.T) {
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
-	testFs := fcfg.Filesystem()
+	testFs := fcfg.Filesystem(nil)
 	defer os.RemoveAll(testFs.URI())
 
 	testCases := map[string]func(protocol.FileInfo) bool{
@@ -2644,7 +2644,7 @@ func TestCustomMarkerName(t *testing.T) {
 func TestRemoveDirWithContent(t *testing.T) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	tfs.MkdirAll("dirwith", 0755)
@@ -2706,7 +2706,7 @@ func TestIssue4475(t *testing.T) {
 	m, conn, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
 	defer cleanupModel(m)
-	testFs := fcfg.Filesystem()
+	testFs := fcfg.Filesystem(nil)
 
 	// Scenario: Dir is deleted locally and before syncing/index exchange
 	// happens, a file is create in that dir on the remote.
@@ -2769,7 +2769,7 @@ func TestVersionRestore(t *testing.T) {
 	fcfg := newFolderConfiguration(defaultCfgWrapper, "default", "default", fs.FilesystemTypeBasic, dir)
 	fcfg.Versioning.Type = "simple"
 	fcfg.FSWatcherEnabled = false
-	filesystem := fcfg.Filesystem()
+	filesystem := fcfg.Filesystem(nil)
 
 	rawConfig := config.Configuration{
 		Folders: []config.FolderConfiguration{fcfg},
@@ -3005,7 +3005,7 @@ func TestIssue4094(t *testing.T) {
 		t.Fatalf("failed setting ignores: %v", err)
 	}
 
-	if _, err := fcfg.Filesystem().Lstat(".stignore"); err != nil {
+	if _, err := fcfg.Filesystem(nil).Lstat(".stignore"); err != nil {
 		t.Fatalf("failed stating .stignore: %v", err)
 	}
 }
@@ -3037,7 +3037,7 @@ func TestIssue4903(t *testing.T) {
 		t.Fatalf("expected path missing error, got: %v", err)
 	}
 
-	if _, err := fcfg.Filesystem().Lstat("."); !fs.IsNotExist(err) {
+	if _, err := fcfg.Filesystem(nil).Lstat("."); !fs.IsNotExist(err) {
 		t.Fatalf("Expected missing path error, got: %v", err)
 	}
 }
@@ -3067,7 +3067,7 @@ func TestParentOfUnignored(t *testing.T) {
 	m, cancel := newState(t, defaultCfg)
 	defer cleanupModel(m)
 	defer cancel()
-	defer defaultFolderConfig.Filesystem().Remove(".stignore")
+	defer defaultFolderConfig.Filesystem(nil).Remove(".stignore")
 
 	m.SetIgnores("default", []string{"!quux", "*"})
 
@@ -3184,7 +3184,7 @@ func TestConnCloseOnRestart(t *testing.T) {
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
 	m := setupModel(t, w)
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	br := &testutils.BlockingRW{}
 	nw := &testutils.NoopRW{}
@@ -3220,7 +3220,7 @@ func TestConnCloseOnRestart(t *testing.T) {
 func TestModTimeWindow(t *testing.T) {
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	fcfg.RawModTimeWindowS = 2
 	setFolder(t, w, fcfg)
 	m := setupModel(t, w)
@@ -3277,7 +3277,7 @@ func TestModTimeWindow(t *testing.T) {
 func TestDevicePause(t *testing.T) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	sub := m.evLogger.Subscribe(events.DevicePaused)
 	defer sub.Unsubscribe()
@@ -3304,7 +3304,7 @@ func TestDevicePause(t *testing.T) {
 func TestDeviceWasSeen(t *testing.T) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	m.deviceWasSeen(device1)
 
@@ -3399,7 +3399,7 @@ func TestRenameSequenceOrder(t *testing.T) {
 
 	numFiles := 20
 
-	ffs := fcfg.Filesystem()
+	ffs := fcfg.Filesystem(nil)
 	for i := 0; i < numFiles; i++ {
 		v := fmt.Sprintf("%d", i)
 		writeFile(t, ffs, v, []byte(v))
@@ -3468,7 +3468,7 @@ func TestRenameSameFile(t *testing.T) {
 	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
-	ffs := fcfg.Filesystem()
+	ffs := fcfg.Filesystem(nil)
 	writeFile(t, ffs, "file", []byte("file"))
 
 	m.ScanFolders()
@@ -3519,7 +3519,7 @@ func TestRenameEmptyFile(t *testing.T) {
 	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
-	ffs := fcfg.Filesystem()
+	ffs := fcfg.Filesystem(nil)
 
 	writeFile(t, ffs, "file", []byte("data"))
 	writeFile(t, ffs, "empty", nil)
@@ -3596,7 +3596,7 @@ func TestBlockListMap(t *testing.T) {
 	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
-	ffs := fcfg.Filesystem()
+	ffs := fcfg.Filesystem(nil)
 	writeFile(t, ffs, "one", []byte("content"))
 	writeFile(t, ffs, "two", []byte("content"))
 	writeFile(t, ffs, "three", []byte("content"))
@@ -3664,7 +3664,7 @@ func TestScanRenameCaseOnly(t *testing.T) {
 	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
-	ffs := fcfg.Filesystem()
+	ffs := fcfg.Filesystem(nil)
 	name := "foo"
 	writeFile(t, ffs, name, []byte("contents"))
 
@@ -3782,7 +3782,7 @@ func TestAddFolderCompletion(t *testing.T) {
 
 func TestScanDeletedROChangedOnSR(t *testing.T) {
 	m, _, fcfg, wCancel := setupModelWithConnection(t)
-	ffs := fcfg.Filesystem()
+	ffs := fcfg.Filesystem(nil)
 	defer wCancel()
 	defer cleanupModelAndRemoveDir(m, ffs.URI())
 	fcfg.Type = config.FolderTypeReceiveOnly
@@ -3886,7 +3886,7 @@ func testConfigChangeTriggersClusterConfigs(t *testing.T, expectFirst, expectSec
 func TestIssue6961(t *testing.T) {
 	wcfg, fcfg, wcfgCancel := tmpDefaultWrapper()
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	waiter, err := wcfg.Modify(func(cfg *config.Configuration) {
 		cfg.SetDevice(newDeviceConfiguration(cfg.Defaults.Device, device2, "device2"))
 		fcfg.Type = config.FolderTypeReceiveOnly
@@ -3956,7 +3956,7 @@ func TestIssue6961(t *testing.T) {
 func TestCompletionEmptyGlobal(t *testing.T) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 	files := []protocol.FileInfo{{Name: "foo", Version: protocol.Vector{}.Update(myID.Short()), Sequence: 1}}
 	m.fmut.Lock()
 	m.folderFiles[fcfg.ID].Update(protocol.LocalDeviceID, files)
@@ -4157,7 +4157,7 @@ func TestCCFolderNotRunning(t *testing.T) {
 	// Create the folder, but don't start it.
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	m := newModel(t, w, myID, "syncthing", "dev", nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
@@ -4264,7 +4264,7 @@ func TestDeletedNotLocallyChangedReceiveEncrypted(t *testing.T) {
 
 func deletedNotLocallyChanged(t *testing.T, ft config.FolderType) {
 	w, fcfg, wCancel := tmpDefaultWrapper()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	fcfg.Type = ft
 	setFolder(t, w, fcfg)
 	defer wCancel()

+ 20 - 20
lib/model/requests_test.go

@@ -32,7 +32,7 @@ func TestRequestSimple(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	// We listen for incoming index updates and trigger when we see one for
@@ -79,7 +79,7 @@ func TestSymlinkTraversalRead(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	// We listen for incoming index updates and trigger when we see one for
 	// the expected test file.
@@ -122,7 +122,7 @@ func TestSymlinkTraversalWrite(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	// We listen for incoming index updates and trigger when we see one for
 	// the expected names.
@@ -181,7 +181,7 @@ func TestRequestCreateTmpSymlink(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	// We listen for incoming index updates and trigger when we see one for
 	// the expected test file.
@@ -224,7 +224,7 @@ func TestRequestVersioningSymlinkAttack(t *testing.T) {
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
 	defer func() {
-		os.RemoveAll(fcfg.Filesystem().URI())
+		os.RemoveAll(fcfg.Filesystem(nil).URI())
 		os.Remove(w.ConfigPath())
 	}()
 
@@ -301,7 +301,7 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
 	w, wCancel := createTmpWrapper(defaultCfgWrapper.RawCopy())
 	defer wCancel()
 	fcfg := testFolderConfigTmp()
-	fss := fcfg.Filesystem()
+	fss := fcfg.Filesystem(nil)
 	fcfg.Type = ft
 	setFolder(t, w, fcfg)
 	m := setupModel(t, w)
@@ -430,7 +430,7 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
 func TestIssue4841(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	received := make(chan []protocol.FileInfo)
 	fc.setIndexFn(func(_ context.Context, _ string, fs []protocol.FileInfo) error {
@@ -478,7 +478,7 @@ func TestIssue4841(t *testing.T) {
 func TestRescanIfHaveInvalidContent(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	payload := []byte("hello")
@@ -544,7 +544,7 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
 func TestParentDeletion(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	testFs := fcfg.Filesystem()
+	testFs := fcfg.Filesystem(nil)
 	defer cleanupModelAndRemoveDir(m, testFs.URI())
 
 	parent := "foo"
@@ -623,7 +623,7 @@ func TestRequestSymlinkWindows(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
 
 	received := make(chan []protocol.FileInfo)
 	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
@@ -691,7 +691,7 @@ func equalContents(path string, contents []byte) error {
 func TestRequestRemoteRenameChanged(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	tmpDir := tfs.URI()
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
@@ -825,7 +825,7 @@ func TestRequestRemoteRenameChanged(t *testing.T) {
 func TestRequestRemoteRenameConflict(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	tmpDir := tfs.URI()
 	defer cleanupModelAndRemoveDir(m, tmpDir)
 
@@ -916,7 +916,7 @@ func TestRequestRemoteRenameConflict(t *testing.T) {
 func TestRequestDeleteChanged(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	done := make(chan struct{})
@@ -984,7 +984,7 @@ func TestRequestDeleteChanged(t *testing.T) {
 func TestNeedFolderFiles(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	tmpDir := tfs.URI()
 	defer cleanupModelAndRemoveDir(m, tmpDir)
 
@@ -1032,7 +1032,7 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
 	m := setupModel(t, w)
-	fss := fcfg.Filesystem()
+	fss := fcfg.Filesystem(nil)
 	tmpDir := fss.URI()
 	defer cleanupModelAndRemoveDir(m, tmpDir)
 
@@ -1127,7 +1127,7 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
 func TestRequestLastFileProgress(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	done := make(chan struct{})
@@ -1162,7 +1162,7 @@ func TestRequestIndexSenderPause(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	indexChan := make(chan []protocol.FileInfo)
@@ -1275,7 +1275,7 @@ func TestRequestIndexSenderPause(t *testing.T) {
 func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	dir1 := "foo"
 	dir2 := "bar"
 
@@ -1342,7 +1342,7 @@ func TestRequestReceiveEncrypted(t *testing.T) {
 
 	w, fcfg, wCancel := tmpDefaultWrapper()
 	defer wCancel()
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	fcfg.Type = config.FolderTypeReceiveEncrypted
 	setFolder(t, w, fcfg)
 
@@ -1450,7 +1450,7 @@ func TestRequestGlobalInvalidToValid(t *testing.T) {
 	must(t, err)
 	waiter.Wait()
 	addFakeConn(m, device2, fcfg.ID)
-	tfs := fcfg.Filesystem()
+	tfs := fcfg.Filesystem(nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	indexChan := make(chan []protocol.FileInfo, 1)

+ 2 - 2
lib/model/testutils_test.go

@@ -40,7 +40,7 @@ func init() {
 	defaultCfgWrapper, defaultCfgWrapperCancel = createTmpWrapper(config.New(myID))
 
 	defaultFolderConfig = testFolderConfig("testdata")
-	defaultFs = defaultFolderConfig.Filesystem()
+	defaultFs = defaultFolderConfig.Filesystem(nil)
 
 	waiter, _ := defaultCfgWrapper.Modify(func(cfg *config.Configuration) {
 		cfg.SetDevice(newDeviceConfiguration(cfg.Defaults.Device, device1, "device1"))
@@ -308,7 +308,7 @@ func folderIgnoresAlwaysReload(t testing.TB, m *testModel, fcfg config.FolderCon
 	t.Helper()
 	m.removeFolder(fcfg)
 	fset := newFileSet(t, fcfg.ID, m.db)
-	ignores := ignore.New(fcfg.Filesystem(), ignore.WithCache(true), ignore.WithChangeDetector(newAlwaysChanged()))
+	ignores := ignore.New(fcfg.Filesystem(nil), ignore.WithCache(true), ignore.WithChangeDetector(newAlwaysChanged()))
 	m.fmut.Lock()
 	m.addAndStartFolderLockedWithIgnores(fcfg, fset, ignores)
 	m.fmut.Unlock()

+ 9 - 5
lib/scanner/walk_test.go

@@ -38,10 +38,14 @@ type testfile struct {
 
 type testfileList []testfile
 
+const (
+	testFsType     = fs.FilesystemTypeBasic
+	testFsLocation = "testdata"
+)
+
 var (
-	testFs     fs.Filesystem
-	testFsType = fs.FilesystemTypeBasic
-	testdata   = testfileList{
+	testFs   fs.Filesystem
+	testdata = testfileList{
 		{"afile", 4, "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"},
 		{"dir1", 128, ""},
 		{filepath.Join("dir1", "dfile"), 5, "49ae93732fcf8d63fe1cce759664982dbd5b23161f007dba8561862adc96d063"},
@@ -58,7 +62,7 @@ func init() {
 	// potentially taking down the box...
 	rdebug.SetMaxStack(10 * 1 << 20)
 
-	testFs = fs.NewFilesystem(fs.FilesystemTypeBasic, "testdata")
+	testFs = fs.NewFilesystem(testFsType, testFsLocation)
 }
 
 func TestWalkSub(t *testing.T) {
@@ -258,7 +262,7 @@ func TestNormalizationDarwinCaseFS(t *testing.T) {
 		return
 	}
 
-	testFs := fs.NewCaseFilesystem(testFs)
+	testFs := fs.NewFilesystem(testFsType, testFsLocation, new(fs.OptionDetectCaseConflicts))
 
 	testFs.RemoveAll("normalization")
 	defer testFs.RemoveAll("normalization")

+ 1 - 1
lib/versioner/external.go

@@ -41,7 +41,7 @@ func newExternal(cfg config.FolderConfiguration) Versioner {
 
 	s := external{
 		command:    command,
-		filesystem: cfg.Filesystem(),
+		filesystem: cfg.Filesystem(nil),
 	}
 
 	l.Debugf("instantiated %#v", s)

+ 1 - 1
lib/versioner/simple.go

@@ -40,7 +40,7 @@ func newSimple(cfg config.FolderConfiguration) Versioner {
 	s := simple{
 		keep:            keep,
 		cleanoutDays:    cleanoutDays,
-		folderFs:        cfg.Filesystem(),
+		folderFs:        cfg.Filesystem(nil),
 		versionsFs:      versionerFsFromFolderCfg(cfg),
 		copyRangeMethod: cfg.CopyRangeMethod,
 	}

+ 1 - 1
lib/versioner/simple_test.go

@@ -70,7 +70,7 @@ func TestSimpleVersioningVersionCount(t *testing.T) {
 			},
 		},
 	}
-	fs := cfg.Filesystem()
+	fs := cfg.Filesystem(nil)
 
 	v := newSimple(cfg)
 

+ 1 - 1
lib/versioner/staggered.go

@@ -44,7 +44,7 @@ func newStaggered(cfg config.FolderConfiguration) Versioner {
 	versionsFs := versionerFsFromFolderCfg(cfg)
 
 	s := &staggered{
-		folderFs:   cfg.Filesystem(),
+		folderFs:   cfg.Filesystem(nil),
 		versionsFs: versionsFs,
 		interval: [4]interval{
 			{30, 60 * 60},                     // first hour -> 30 sec between versions

+ 1 - 1
lib/versioner/trashcan.go

@@ -33,7 +33,7 @@ func newTrashcan(cfg config.FolderConfiguration) Versioner {
 	// On error we default to 0, "do not clean out the trash can"
 
 	s := &trashcan{
-		folderFs:        cfg.Filesystem(),
+		folderFs:        cfg.Filesystem(nil),
 		versionsFs:      versionerFsFromFolderCfg(cfg),
 		cleanoutDays:    cleanoutDays,
 		copyRangeMethod: cfg.CopyRangeMethod,

+ 1 - 1
lib/versioner/trashcan_test.go

@@ -38,7 +38,7 @@ func TestTrashcanArchiveRestoreSwitcharoo(t *testing.T) {
 			FSPath: tmpDir2,
 		},
 	}
-	folderFs := cfg.Filesystem()
+	folderFs := cfg.Filesystem(nil)
 
 	versionsFs := fs.NewFilesystem(fs.FilesystemTypeBasic, tmpDir2)
 

+ 1 - 1
lib/versioner/util.go

@@ -256,7 +256,7 @@ func restoreFile(method fs.CopyRangeMethod, src, dst fs.Filesystem, filePath str
 }
 
 func versionerFsFromFolderCfg(cfg config.FolderConfiguration) (versionsFs fs.Filesystem) {
-	folderFs := cfg.Filesystem()
+	folderFs := cfg.Filesystem(nil)
 	if cfg.Versioning.FSPath == "" {
 		versionsFs = fs.NewFilesystem(folderFs.Type(), filepath.Join(folderFs.URI(), ".stversions"))
 	} else if cfg.Versioning.FSType == fs.FilesystemTypeBasic && !filepath.IsAbs(cfg.Versioning.FSPath) {