diff --git a/cmd/syncthing/cli/client.go b/cmd/syncthing/cli/client.go index 9bf0ab993..09d62c9a7 100644 --- a/cmd/syncthing/cli/client.go +++ b/cmd/syncthing/cli/client.go @@ -135,11 +135,11 @@ func (c *apiClient) Post(url, body string) (*http.Response, error) { } func checkResponse(response *http.Response) error { - if response.StatusCode == 404 { + if response.StatusCode == http.StatusNotFound { return errors.New("invalid endpoint or API call") - } else if response.StatusCode == 403 { + } else if response.StatusCode == http.StatusUnauthorized { return errors.New("invalid API key") - } else if response.StatusCode != 200 { + } else if response.StatusCode != http.StatusOK { data, err := responseToBArray(response) if err != nil { return err diff --git a/lib/api/api.go b/lib/api/api.go index 127c01a0a..0b59b27b9 100644 --- a/lib/api/api.go +++ b/lib/api/api.go @@ -32,7 +32,7 @@ import ( "unicode" "github.com/julienschmidt/httprouter" - metrics "github.com/rcrowley/go-metrics" + "github.com/rcrowley/go-metrics" "github.com/thejerf/suture/v4" "github.com/vitrun/qart/qr" "golang.org/x/text/runes" @@ -915,11 +915,16 @@ func (s *service) getDBFile(w http.ResponseWriter, r *http.Request) { if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } + mtimeMapping, mtimeErr := s.model.GetMtimeMapping(folder, file) sendJSON(w, map[string]interface{}{ "global": jsonFileInfo(gf), "local": jsonFileInfo(lf), "availability": av, + "mtime": map[string]interface{}{ + "err": mtimeErr, + "value": mtimeMapping, + }, }) } @@ -934,6 +939,8 @@ func (s *service) getDebugFile(w http.ResponseWriter, r *http.Request) { return } + mtimeMapping, mtimeErr := s.model.GetMtimeMapping(folder, file) + lf, _ := snap.Get(protocol.LocalDeviceID, file) gf, _ := snap.GetGlobal(file) av := snap.Availability(file) @@ -944,6 +951,10 @@ func (s *service) getDebugFile(w http.ResponseWriter, r *http.Request) { "local": jsonFileInfo(lf), "availability": av, "globalVersions": vl.String(), + "mtime": map[string]interface{}{ + "err": mtimeErr, + "value": mtimeMapping, + }, }) } diff --git a/lib/db/keyer.go b/lib/db/keyer.go index 2c88cf9cb..fac2d1af3 100644 --- a/lib/db/keyer.go +++ b/lib/db/keyer.go @@ -36,7 +36,7 @@ const ( // KeyTypeFolderStatistic = some value KeyTypeFolderStatistic byte = 4 - // KeyTypeVirtualMtime = dbMtime + // KeyTypeVirtualMtime = mtimeMapping KeyTypeVirtualMtime byte = 5 // KeyTypeFolderIdx = string value diff --git a/lib/fs/basicfs.go b/lib/fs/basicfs.go index 2cf4b28b1..db5bb97cb 100644 --- a/lib/fs/basicfs.go +++ b/lib/fs/basicfs.go @@ -330,6 +330,14 @@ func (f *BasicFilesystem) SameFile(fi1, fi2 FileInfo) bool { return os.SameFile(f1.osFileInfo(), f2.osFileInfo()) } +func (f *BasicFilesystem) underlying() (Filesystem, bool) { + return nil, false +} + +func (f *BasicFilesystem) wrapperType() filesystemWrapperType { + return filesystemWrapperTypeNone +} + // basicFile implements the fs.File interface on top of an os.File type basicFile struct { *os.File diff --git a/lib/fs/casefs.go b/lib/fs/casefs.go index 1cdb0a7c1..e04a11753 100644 --- a/lib/fs/casefs.go +++ b/lib/fs/casefs.go @@ -339,6 +339,14 @@ func (f *caseFilesystem) Unhide(name string) error { return f.Filesystem.Unhide(name) } +func (f *caseFilesystem) underlying() (Filesystem, bool) { + return f.Filesystem, true +} + +func (f *caseFilesystem) wrapperType() filesystemWrapperType { + return filesystemWrapperTypeCase +} + func (f *caseFilesystem) checkCase(name string) error { var err error if name, err = Canonicalize(name); err != nil { diff --git a/lib/fs/casefs_test.go b/lib/fs/casefs_test.go index ebf776397..9c5915db8 100644 --- a/lib/fs/casefs_test.go +++ b/lib/fs/casefs_test.go @@ -161,7 +161,10 @@ func BenchmarkWalkCaseFakeFS100k(b *testing.B) { b.Fatal(err) } b.Run("rawfs", func(b *testing.B) { - fakefs := unwrapFilesystem(fsys).(*fakefs) + var fakefs *fakeFS + if ffs, ok := unwrapFilesystem(fsys, filesystemWrapperTypeNone); ok { + fakefs = ffs.(*fakeFS) + } fakefs.resetCounters() benchmarkWalkFakeFS(b, fsys, paths, 0, "") fakefs.reportMetricsPerOp(b) @@ -174,7 +177,10 @@ func BenchmarkWalkCaseFakeFS100k(b *testing.B) { Filesystem: fsys, realCaser: newDefaultRealCaser(fsys), } - fakefs := unwrapFilesystem(fsys).(*fakefs) + var fakefs *fakeFS + if ffs, ok := unwrapFilesystem(fsys, filesystemWrapperTypeNone); ok { + fakefs = ffs.(*fakeFS) + } fakefs.resetCounters() benchmarkWalkFakeFS(b, casefs, paths, 0, "") fakefs.reportMetricsPerOp(b) @@ -197,7 +203,10 @@ func BenchmarkWalkCaseFakeFS100k(b *testing.B) { Filesystem: fsys, realCaser: newDefaultRealCaser(fsys), } - fakefs := unwrapFilesystem(fsys).(*fakefs) + var fakefs *fakeFS + if ffs, ok := unwrapFilesystem(fsys, filesystemWrapperTypeNone); ok { + fakefs = ffs.(*fakeFS) + } fakefs.resetCounters() benchmarkWalkFakeFS(b, casefs, paths, otherOpEvery, otherOpPath) fakefs.reportMetricsPerOp(b) diff --git a/lib/fs/debug_symlink_unix.go b/lib/fs/debug_symlink_unix.go index e0f3777ae..d075197eb 100644 --- a/lib/fs/debug_symlink_unix.go +++ b/lib/fs/debug_symlink_unix.go @@ -18,7 +18,8 @@ import ( // reason for existence is the Windows version, which allows creating // symlinks when non-elevated. func DebugSymlinkForTestsOnly(oldFs, newFs Filesystem, oldname, newname string) error { - if caseFs, ok := unwrapFilesystem(newFs).(*caseFilesystem); ok { + if fs, ok := unwrapFilesystem(newFs, filesystemWrapperTypeCase); ok { + caseFs := fs.(*caseFilesystem) if err := caseFs.checkCase(newname); err != nil { return err } diff --git a/lib/fs/errorfs.go b/lib/fs/errorfs.go index d1ed43867..1c9965af5 100644 --- a/lib/fs/errorfs.go +++ b/lib/fs/errorfs.go @@ -52,3 +52,11 @@ 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 } + +func (fs *errorFilesystem) underlying() (Filesystem, bool) { + return nil, false +} + +func (fs *errorFilesystem) wrapperType() filesystemWrapperType { + return filesystemWrapperTypeError +} diff --git a/lib/fs/fakefs.go b/lib/fs/fakefs.go index d8edc15cd..5e259272d 100644 --- a/lib/fs/fakefs.go +++ b/lib/fs/fakefs.go @@ -27,7 +27,7 @@ import ( // see readShortAt() const randomBlockShift = 14 // 128k -// fakefs is a fake filesystem for testing and benchmarking. It has the +// fakeFS is a fake filesystem for testing and benchmarking. It has the // following properties: // // - File metadata is kept in RAM. Specifically, we remember which files and @@ -37,7 +37,7 @@ const randomBlockShift = 14 // 128k // - File contents are generated pseudorandomly with just the file name as // seed. Writes are discarded, other than having the effect of increasing // the file size. If you only write data that you've read from a file with -// the same name on a different fakefs, you'll never know the difference... +// the same name on a different fakeFS, you'll never know the difference... // // - We totally ignore permissions - pretend you are root. // @@ -51,10 +51,10 @@ const randomBlockShift = 14 // 128k // insens=b "true" makes filesystem case-insensitive Windows- or OSX-style (default false) // latency=d to set the amount of time each "disk" operation takes, where d is time.ParseDuration format // -// - Two fakefs:s pointing at the same root path see the same files. +// - Two fakeFS:s pointing at the same root path see the same files. // -type fakefs struct { - counters fakefsCounters +type fakeFS struct { + counters fakeFSCounters uri string mut sync.Mutex root *fakeEntry @@ -63,7 +63,7 @@ type fakefs struct { latency time.Duration } -type fakefsCounters struct { +type fakeFSCounters struct { Chmod int64 Lchown int64 Chtimes int64 @@ -81,13 +81,13 @@ type fakefsCounters struct { } var ( - fakefsMut sync.Mutex - fakefsFs = make(map[string]*fakefs) + fakeFSMut sync.Mutex + fakeFSCache = make(map[string]*fakeFS) ) -func newFakeFilesystem(rootURI string, _ ...Option) *fakefs { - fakefsMut.Lock() - defer fakefsMut.Unlock() +func newFakeFilesystem(rootURI string, _ ...Option) *fakeFS { + fakeFSMut.Lock() + defer fakeFSMut.Unlock() root := rootURI var params url.Values @@ -97,12 +97,12 @@ func newFakeFilesystem(rootURI string, _ ...Option) *fakefs { params = uri.Query() } - if fs, ok := fakefsFs[rootURI]; ok { + if fs, ok := fakeFSCache[rootURI]; ok { // Already have an fs at this path return fs } - fs := &fakefs{ + fs := &fakeFS{ uri: "fake://" + rootURI, root: &fakeEntry{ name: "/", @@ -157,7 +157,7 @@ func newFakeFilesystem(rootURI string, _ ...Option) *fakefs { // the filesystem initially. fs.latency, _ = time.ParseDuration(params.Get("latency")) - fakefsFs[root] = fs + fakeFSCache[root] = fs return fs } @@ -183,7 +183,7 @@ type fakeEntry struct { content []byte } -func (fs *fakefs) entryForName(name string) *fakeEntry { +func (fs *fakeFS) entryForName(name string) *fakeEntry { // bug: lookup doesn't work through symlinks. if fs.insens { name = UnicodeLowercase(name) @@ -210,7 +210,7 @@ func (fs *fakefs) entryForName(name string) *fakeEntry { return entry } -func (fs *fakefs) Chmod(name string, mode FileMode) error { +func (fs *fakeFS) Chmod(name string, mode FileMode) error { fs.mut.Lock() defer fs.mut.Unlock() fs.counters.Chmod++ @@ -223,7 +223,7 @@ func (fs *fakefs) Chmod(name string, mode FileMode) error { return nil } -func (fs *fakefs) Lchown(name string, uid, gid int) error { +func (fs *fakeFS) Lchown(name string, uid, gid int) error { fs.mut.Lock() defer fs.mut.Unlock() fs.counters.Lchown++ @@ -237,7 +237,7 @@ func (fs *fakefs) Lchown(name string, uid, gid int) error { return nil } -func (fs *fakefs) Chtimes(name string, atime time.Time, mtime time.Time) error { +func (fs *fakeFS) Chtimes(name string, atime time.Time, mtime time.Time) error { fs.mut.Lock() defer fs.mut.Unlock() fs.counters.Chtimes++ @@ -250,7 +250,7 @@ func (fs *fakefs) Chtimes(name string, atime time.Time, mtime time.Time) error { return nil } -func (fs *fakefs) create(name string) (*fakeEntry, error) { +func (fs *fakeFS) create(name string) (*fakeEntry, error) { fs.mut.Lock() defer fs.mut.Unlock() fs.counters.Create++ @@ -296,7 +296,7 @@ func (fs *fakefs) create(name string) (*fakeEntry, error) { return new, nil } -func (fs *fakefs) Create(name string) (File, error) { +func (fs *fakeFS) Create(name string) (File, error) { entry, err := fs.create(name) if err != nil { return nil, err @@ -307,7 +307,7 @@ func (fs *fakefs) Create(name string) (File, error) { return &fakeFile{fakeEntry: entry}, nil } -func (fs *fakefs) CreateSymlink(target, name string) error { +func (fs *fakeFS) CreateSymlink(target, name string) error { entry, err := fs.create(name) if err != nil { return err @@ -317,7 +317,7 @@ func (fs *fakefs) CreateSymlink(target, name string) error { return nil } -func (fs *fakefs) DirNames(name string) ([]string, error) { +func (fs *fakeFS) DirNames(name string) ([]string, error) { fs.mut.Lock() defer fs.mut.Unlock() fs.counters.DirNames++ @@ -336,7 +336,7 @@ func (fs *fakefs) DirNames(name string) ([]string, error) { return names, nil } -func (fs *fakefs) Lstat(name string) (FileInfo, error) { +func (fs *fakeFS) Lstat(name string) (FileInfo, error) { fs.mut.Lock() defer fs.mut.Unlock() fs.counters.Lstat++ @@ -355,7 +355,7 @@ func (fs *fakefs) Lstat(name string) (FileInfo, error) { return info, nil } -func (fs *fakefs) Mkdir(name string, perm FileMode) error { +func (fs *fakeFS) Mkdir(name string, perm FileMode) error { fs.mut.Lock() defer fs.mut.Unlock() fs.counters.Mkdir++ @@ -389,7 +389,7 @@ func (fs *fakefs) Mkdir(name string, perm FileMode) error { return nil } -func (fs *fakefs) MkdirAll(name string, perm FileMode) error { +func (fs *fakeFS) MkdirAll(name string, perm FileMode) error { fs.mut.Lock() defer fs.mut.Unlock() fs.counters.MkdirAll++ @@ -426,7 +426,7 @@ func (fs *fakefs) MkdirAll(name string, perm FileMode) error { return nil } -func (fs *fakefs) Open(name string) (File, error) { +func (fs *fakeFS) Open(name string) (File, error) { fs.mut.Lock() defer fs.mut.Unlock() fs.counters.Open++ @@ -443,7 +443,7 @@ func (fs *fakefs) Open(name string) (File, error) { return &fakeFile{fakeEntry: entry}, nil } -func (fs *fakefs) OpenFile(name string, flags int, mode FileMode) (File, error) { +func (fs *fakeFS) OpenFile(name string, flags int, mode FileMode) (File, error) { if flags&os.O_CREATE == 0 { return fs.Open(name) } @@ -486,7 +486,7 @@ func (fs *fakefs) OpenFile(name string, flags int, mode FileMode) (File, error) return &fakeFile{fakeEntry: newEntry}, nil } -func (fs *fakefs) ReadSymlink(name string) (string, error) { +func (fs *fakeFS) ReadSymlink(name string) (string, error) { fs.mut.Lock() defer fs.mut.Unlock() fs.counters.ReadSymlink++ @@ -501,7 +501,7 @@ func (fs *fakefs) ReadSymlink(name string) (string, error) { return entry.dest, nil } -func (fs *fakefs) Remove(name string) error { +func (fs *fakeFS) Remove(name string) error { fs.mut.Lock() defer fs.mut.Unlock() fs.counters.Remove++ @@ -524,7 +524,7 @@ func (fs *fakefs) Remove(name string) error { return nil } -func (fs *fakefs) RemoveAll(name string) error { +func (fs *fakeFS) RemoveAll(name string) error { fs.mut.Lock() defer fs.mut.Unlock() fs.counters.RemoveAll++ @@ -536,7 +536,7 @@ func (fs *fakefs) RemoveAll(name string) error { entry := fs.entryForName(filepath.Dir(name)) if entry == nil { - return nil // all tested real systems exibit this behaviour + return nil // all tested real systems exhibit this behaviour } // RemoveAll is easy when the file system uses garbage collection under @@ -545,7 +545,7 @@ func (fs *fakefs) RemoveAll(name string) error { return nil } -func (fs *fakefs) Rename(oldname, newname string) error { +func (fs *fakeFS) Rename(oldname, newname string) error { fs.mut.Lock() defer fs.mut.Unlock() fs.counters.Rename++ @@ -595,56 +595,56 @@ func (fs *fakefs) Rename(oldname, newname string) error { return nil } -func (fs *fakefs) Stat(name string) (FileInfo, error) { +func (fs *fakeFS) Stat(name string) (FileInfo, error) { return fs.Lstat(name) } -func (fs *fakefs) SymlinksSupported() bool { +func (fs *fakeFS) SymlinksSupported() bool { return false } -func (fs *fakefs) Walk(name string, walkFn WalkFunc) error { +func (fs *fakeFS) Walk(name string, walkFn WalkFunc) error { return errors.New("not implemented") } -func (fs *fakefs) Watch(path string, ignore Matcher, ctx context.Context, ignorePerms bool) (<-chan Event, <-chan error, error) { +func (fs *fakeFS) Watch(path string, ignore Matcher, ctx context.Context, ignorePerms bool) (<-chan Event, <-chan error, error) { return nil, nil, ErrWatchNotSupported } -func (fs *fakefs) Hide(name string) error { +func (fs *fakeFS) Hide(name string) error { return nil } -func (fs *fakefs) Unhide(name string) error { +func (fs *fakeFS) Unhide(name string) error { return nil } -func (fs *fakefs) Glob(pattern string) ([]string, error) { +func (fs *fakeFS) Glob(pattern string) ([]string, error) { // gnnh we don't seem to actually require this in practice return nil, errors.New("not implemented") } -func (fs *fakefs) Roots() ([]string, error) { +func (fs *fakeFS) Roots() ([]string, error) { return []string{"/"}, nil } -func (fs *fakefs) Usage(name string) (Usage, error) { +func (fs *fakeFS) Usage(name string) (Usage, error) { return Usage{}, errors.New("not implemented") } -func (fs *fakefs) Type() FilesystemType { +func (fs *fakeFS) Type() FilesystemType { return FilesystemTypeFake } -func (fs *fakefs) URI() string { +func (fs *fakeFS) URI() string { return fs.uri } -func (fs *fakefs) Options() []Option { +func (fs *fakeFS) Options() []Option { return nil } -func (fs *fakefs) SameFile(fi1, fi2 FileInfo) bool { +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 // so there be false positives, especially on Windows @@ -659,17 +659,25 @@ func (fs *fakefs) SameFile(fi1, fi2 FileInfo) bool { return ok && fi1.ModTime().Equal(fi2.ModTime()) && fi1.Mode() == fi2.Mode() && fi1.IsDir() == fi2.IsDir() && fi1.IsRegular() == fi2.IsRegular() && fi1.IsSymlink() == fi2.IsSymlink() && fi1.Owner() == fi2.Owner() && fi1.Group() == fi2.Group() } -func (fs *fakefs) resetCounters() { +func (fs *fakeFS) underlying() (Filesystem, bool) { + return nil, false +} + +func (fs *fakeFS) wrapperType() filesystemWrapperType { + return filesystemWrapperTypeNone +} + +func (fs *fakeFS) resetCounters() { fs.mut.Lock() - fs.counters = fakefsCounters{} + fs.counters = fakeFSCounters{} fs.mut.Unlock() } -func (fs *fakefs) reportMetricsPerOp(b *testing.B) { +func (fs *fakeFS) reportMetricsPerOp(b *testing.B) { fs.reportMetricsPer(b, 1, "op") } -func (fs *fakefs) reportMetricsPer(b *testing.B, divisor float64, unit string) { +func (fs *fakeFS) reportMetricsPer(b *testing.B, divisor float64, unit string) { fs.mut.Lock() defer fs.mut.Unlock() b.ReportMetric(float64(fs.counters.Lstat)/divisor/float64(b.N), "Lstat/"+unit) diff --git a/lib/fs/fakefs_test.go b/lib/fs/fakefs_test.go index d7b897ae6..1b095f881 100644 --- a/lib/fs/fakefs_test.go +++ b/lib/fs/fakefs_test.go @@ -21,7 +21,7 @@ import ( ) func TestFakeFS(t *testing.T) { - // Test some basic aspects of the fakefs + // Test some basic aspects of the fakeFS fs := newFakeFilesystem("/foo/bar/baz") @@ -131,7 +131,7 @@ func TestFakeFS(t *testing.T) { } func testFakeFSRead(t *testing.T, fs Filesystem) { - // Test some basic aspects of the fakefs + // Test some basic aspects of the fakeFS // Create fd, _ := fs.Create("test") defer fd.Close() @@ -201,7 +201,7 @@ func TestFakeFSCaseSensitive(t *testing.T) { {"FileName", testFakeFSFileName}, } var filesystems = []testFS{ - {"fakefs", newFakeFilesystem("/foo")}, + {"fakeFS", newFakeFilesystem("/foo")}, } testDir, sensitive := createTestDir(t) @@ -237,7 +237,7 @@ func TestFakeFSCaseInsensitive(t *testing.T) { } var filesystems = []testFS{ - {"fakefs", newFakeFilesystem("/foobar?insens=true")}, + {"fakeFS", newFakeFilesystem("/foobar?insens=true")}, } testDir, sensitive := createTestDir(t) @@ -891,7 +891,7 @@ func testFakeFSCreateInsens(t *testing.T, fs Filesystem) { t.Errorf("name of created file \"fOo\" is %s", fd2.Name()) } - // one would expect DirNames to show the last variant, but in fact it shows + // one would expect DirNames to show the last wrapperType, but in fact it shows // the original one assertDir(t, fs, "/", []string{"FOO"}) } diff --git a/lib/fs/filesystem.go b/lib/fs/filesystem.go index 506ed71b5..7ab08600f 100644 --- a/lib/fs/filesystem.go +++ b/lib/fs/filesystem.go @@ -16,6 +16,17 @@ import ( "time" ) +type filesystemWrapperType int32 + +const ( + filesystemWrapperTypeNone filesystemWrapperType = iota + filesystemWrapperTypeMtime + filesystemWrapperTypeCase + filesystemWrapperTypeError + filesystemWrapperTypeWalk + filesystemWrapperTypeLog +) + // The Filesystem interface abstracts access to the file system. type Filesystem interface { Chmod(name string, mode FileMode) error @@ -49,6 +60,10 @@ type Filesystem interface { URI() string Options() []Option SameFile(fi1, fi2 FileInfo) bool + + // Used for unwrapping things + underlying() (Filesystem, bool) + wrapperType() filesystemWrapperType } // The File interface abstracts access to a regular file, being a somewhat @@ -284,18 +299,16 @@ func wrapFilesystem(fs Filesystem, wrapFn func(Filesystem) Filesystem) Filesyste return fs } -// unwrapFilesystem removes "wrapping" filesystems to expose the underlying filesystem. -func unwrapFilesystem(fs Filesystem) Filesystem { +// 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 for { - switch sfs := fs.(type) { - case *logFilesystem: - fs = sfs.Filesystem - case *walkFilesystem: - fs = sfs.Filesystem - case *mtimeFS: - fs = sfs.Filesystem - default: - return sfs + if fs.wrapperType() == wrapperType { + return fs, true + } + fs, ok = fs.underlying() + if !ok { + return nil, false } } } diff --git a/lib/fs/logfs.go b/lib/fs/logfs.go index 8e8716a1a..a3fe1a0ac 100644 --- a/lib/fs/logfs.go +++ b/lib/fs/logfs.go @@ -163,3 +163,11 @@ func (fs *logFilesystem) Usage(name string) (Usage, error) { l.Debugln(getCaller(), fs.Type(), fs.URI(), "Usage", name, usage, err) return usage, err } + +func (fs *logFilesystem) underlying() (Filesystem, bool) { + return fs.Filesystem, true +} + +func (fs *logFilesystem) wrapperType() filesystemWrapperType { + return filesystemWrapperTypeLog +} diff --git a/lib/fs/mtimefs.go b/lib/fs/mtimefs.go index c41da08ed..f298b7957 100644 --- a/lib/fs/mtimefs.go +++ b/lib/fs/mtimefs.go @@ -7,6 +7,7 @@ package fs import ( + "errors" "time" ) @@ -69,14 +70,14 @@ func (f *mtimeFS) Stat(name string) (FileInfo, error) { return nil, err } - real, virtual, err := f.load(name) + mtimeMapping, err := f.load(name) if err != nil { return nil, err } - if real == info.ModTime() { + if mtimeMapping.Real == info.ModTime() { info = mtimeFileInfo{ FileInfo: info, - mtime: virtual, + mtime: mtimeMapping.Virtual, } } @@ -89,14 +90,14 @@ func (f *mtimeFS) Lstat(name string) (FileInfo, error) { return nil, err } - real, virtual, err := f.load(name) + mtimeMapping, err := f.load(name) if err != nil { return nil, err } - if real == info.ModTime() { + if mtimeMapping.Real == info.ModTime() { info = mtimeFileInfo{ FileInfo: info, - mtime: virtual, + mtime: mtimeMapping.Virtual, } } @@ -106,15 +107,15 @@ func (f *mtimeFS) Lstat(name string) (FileInfo, error) { 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 { - real, virtual, loadErr := f.load(path) + mtimeMapping, loadErr := f.load(path) if loadErr != nil && err == nil { // The iterator gets to deal with the error err = loadErr } - if real == info.ModTime() { + if mtimeMapping.Real == info.ModTime() { info = mtimeFileInfo{ FileInfo: info, - mtime: virtual, + mtime: mtimeMapping.Virtual, } } } @@ -146,8 +147,13 @@ func (f *mtimeFS) OpenFile(name string, flags int, mode FileMode) (File, error) return mtimeFile{fd, f}, nil } -// "real" is the on disk timestamp -// "virtual" is what want the timestamp to be +func (f *mtimeFS) underlying() (Filesystem, bool) { + return f.Filesystem, true +} + +func (f *mtimeFS) wrapperType() filesystemWrapperType { + return filesystemWrapperTypeMtime +} func (f *mtimeFS) save(name string, real, virtual time.Time) { if f.caseInsensitive { @@ -161,32 +167,31 @@ func (f *mtimeFS) save(name string, real, virtual time.Time) { return } - mtime := dbMtime{ - real: real, - virtual: virtual, + mtime := MtimeMapping{ + Real: real, + Virtual: virtual, } bs, _ := mtime.Marshal() // Can't fail f.db.PutBytes(name, bs) } -func (f *mtimeFS) load(name string) (real, virtual time.Time, err error) { +func (f *mtimeFS) load(name string) (MtimeMapping, error) { if f.caseInsensitive { name = UnicodeLowercase(name) } data, exists, err := f.db.Bytes(name) if err != nil { - return time.Time{}, time.Time{}, err + return MtimeMapping{}, err } else if !exists { - return time.Time{}, time.Time{}, nil + return MtimeMapping{}, nil } - var mtime dbMtime + var mtime MtimeMapping if err := mtime.Unmarshal(data); err != nil { - return time.Time{}, time.Time{}, err + return MtimeMapping{}, err } - - return mtime.real, mtime.virtual, nil + return mtime, nil } // The mtimeFileInfo is an os.FileInfo that lies about the ModTime(). @@ -211,43 +216,57 @@ func (f mtimeFile) Stat() (FileInfo, error) { return nil, err } - real, virtual, err := f.fs.load(f.Name()) + mtimeMapping, err := f.fs.load(f.Name()) if err != nil { return nil, err } - if real == info.ModTime() { + if mtimeMapping.Real == info.ModTime() { info = mtimeFileInfo{ FileInfo: info, - mtime: virtual, + mtime: mtimeMapping.Virtual, } } return info, nil } +// Used by copyRange to unwrap to the real file and access SyscallConn func (f mtimeFile) unwrap() File { return f.File } -// The dbMtime is our database representation - -type dbMtime struct { - real time.Time - virtual time.Time +// MtimeMapping represents the mapping as stored in the database +type MtimeMapping struct { + // "Real" is the on disk timestamp + Real time.Time `json:"real"` + // "Virtual" is what want the timestamp to be + Virtual time.Time `json:"virtual"` } -func (t *dbMtime) Marshal() ([]byte, error) { - bs0, _ := t.real.MarshalBinary() - bs1, _ := t.virtual.MarshalBinary() +func (t *MtimeMapping) Marshal() ([]byte, error) { + bs0, _ := t.Real.MarshalBinary() + bs1, _ := t.Virtual.MarshalBinary() return append(bs0, bs1...), nil } -func (t *dbMtime) Unmarshal(bs []byte) error { - if err := t.real.UnmarshalBinary(bs[:len(bs)/2]); err != nil { +func (t *MtimeMapping) Unmarshal(bs []byte) error { + if err := t.Real.UnmarshalBinary(bs[:len(bs)/2]); err != nil { return err } - if err := t.virtual.UnmarshalBinary(bs[len(bs)/2:]); err != nil { + if err := t.Virtual.UnmarshalBinary(bs[len(bs)/2:]); err != nil { return err } return nil } + +func GetMtimeMapping(fs Filesystem, file string) (MtimeMapping, error) { + fs, ok := unwrapFilesystem(fs, filesystemWrapperTypeMtime) + if !ok { + return MtimeMapping{}, errors.New("failed to unwrap") + } + mtimeFs, ok := fs.(*mtimeFS) + if !ok { + return MtimeMapping{}, errors.New("unwrapping failed") + } + return mtimeFs.load(file) +} diff --git a/lib/fs/walkfs.go b/lib/fs/walkfs.go index 27cdb9667..176209f17 100644 --- a/lib/fs/walkfs.go +++ b/lib/fs/walkfs.go @@ -149,3 +149,11 @@ func (f *walkFilesystem) Walk(root string, walkFn WalkFunc) error { } return f.walk(root, info, walkFn, ancestors) } + +func (f *walkFilesystem) underlying() (Filesystem, bool) { + return f.Filesystem, true +} + +func (f *walkFilesystem) wrapperType() filesystemWrapperType { + return filesystemWrapperTypeWalk +} diff --git a/lib/model/mocks/model.go b/lib/model/mocks/model.go index 8491d87d1..31b0749f6 100644 --- a/lib/model/mocks/model.go +++ b/lib/model/mocks/model.go @@ -8,6 +8,7 @@ import ( "time" "github.com/syncthing/syncthing/lib/db" + "github.com/syncthing/syncthing/lib/fs" "github.com/syncthing/syncthing/lib/model" "github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/stats" @@ -249,6 +250,20 @@ type Model struct { getHelloReturnsOnCall map[int]struct { result1 protocol.HelloIntf } + GetMtimeMappingStub func(string, string) (fs.MtimeMapping, error) + getMtimeMappingMutex sync.RWMutex + getMtimeMappingArgsForCall []struct { + arg1 string + arg2 string + } + getMtimeMappingReturns struct { + result1 fs.MtimeMapping + result2 error + } + getMtimeMappingReturnsOnCall map[int]struct { + result1 fs.MtimeMapping + result2 error + } GlobalDirectoryTreeStub func(string, string, int, bool) ([]*model.TreeEntry, error) globalDirectoryTreeMutex sync.RWMutex globalDirectoryTreeArgsForCall []struct { @@ -1691,6 +1706,71 @@ func (fake *Model) GetHelloReturnsOnCall(i int, result1 protocol.HelloIntf) { }{result1} } +func (fake *Model) GetMtimeMapping(arg1 string, arg2 string) (fs.MtimeMapping, error) { + fake.getMtimeMappingMutex.Lock() + ret, specificReturn := fake.getMtimeMappingReturnsOnCall[len(fake.getMtimeMappingArgsForCall)] + fake.getMtimeMappingArgsForCall = append(fake.getMtimeMappingArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + stub := fake.GetMtimeMappingStub + fakeReturns := fake.getMtimeMappingReturns + fake.recordInvocation("GetMtimeMapping", []interface{}{arg1, arg2}) + fake.getMtimeMappingMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *Model) GetMtimeMappingCallCount() int { + fake.getMtimeMappingMutex.RLock() + defer fake.getMtimeMappingMutex.RUnlock() + return len(fake.getMtimeMappingArgsForCall) +} + +func (fake *Model) GetMtimeMappingCalls(stub func(string, string) (fs.MtimeMapping, error)) { + fake.getMtimeMappingMutex.Lock() + defer fake.getMtimeMappingMutex.Unlock() + fake.GetMtimeMappingStub = stub +} + +func (fake *Model) GetMtimeMappingArgsForCall(i int) (string, string) { + fake.getMtimeMappingMutex.RLock() + defer fake.getMtimeMappingMutex.RUnlock() + argsForCall := fake.getMtimeMappingArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *Model) GetMtimeMappingReturns(result1 fs.MtimeMapping, result2 error) { + fake.getMtimeMappingMutex.Lock() + defer fake.getMtimeMappingMutex.Unlock() + fake.GetMtimeMappingStub = nil + fake.getMtimeMappingReturns = struct { + result1 fs.MtimeMapping + result2 error + }{result1, result2} +} + +func (fake *Model) GetMtimeMappingReturnsOnCall(i int, result1 fs.MtimeMapping, result2 error) { + fake.getMtimeMappingMutex.Lock() + defer fake.getMtimeMappingMutex.Unlock() + fake.GetMtimeMappingStub = nil + if fake.getMtimeMappingReturnsOnCall == nil { + fake.getMtimeMappingReturnsOnCall = make(map[int]struct { + result1 fs.MtimeMapping + result2 error + }) + } + fake.getMtimeMappingReturnsOnCall[i] = struct { + result1 fs.MtimeMapping + result2 error + }{result1, result2} +} + func (fake *Model) GlobalDirectoryTree(arg1 string, arg2 string, arg3 int, arg4 bool) ([]*model.TreeEntry, error) { fake.globalDirectoryTreeMutex.Lock() ret, specificReturn := fake.globalDirectoryTreeReturnsOnCall[len(fake.globalDirectoryTreeArgsForCall)] @@ -3186,6 +3266,8 @@ func (fake *Model) Invocations() map[string][][]interface{} { defer fake.getFolderVersionsMutex.RUnlock() fake.getHelloMutex.RLock() defer fake.getHelloMutex.RUnlock() + fake.getMtimeMappingMutex.RLock() + defer fake.getMtimeMappingMutex.RUnlock() fake.globalDirectoryTreeMutex.RLock() defer fake.globalDirectoryTreeMutex.RUnlock() fake.indexMutex.RLock() diff --git a/lib/model/model.go b/lib/model/model.go index d16ad434e..708f571a3 100644 --- a/lib/model/model.go +++ b/lib/model/model.go @@ -95,6 +95,7 @@ type Model interface { CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool, error) CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool, error) + GetMtimeMapping(folder string, file string) (fs.MtimeMapping, error) Availability(folder string, file protocol.FileInfo, block protocol.BlockInfo) ([]Availability, error) Completion(device protocol.DeviceID, folder string) (FolderCompletion, error) @@ -2038,12 +2039,12 @@ func (m *model) CurrentFolderFile(folder string, file string) (protocol.FileInfo func (m *model) CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool, error) { m.fmut.RLock() - fs, ok := m.folderFiles[folder] + ffs, ok := m.folderFiles[folder] m.fmut.RUnlock() if !ok { return protocol.FileInfo{}, false, ErrFolderMissing } - snap, err := fs.Snapshot() + snap, err := ffs.Snapshot() if err != nil { return protocol.FileInfo{}, false, err } @@ -2052,6 +2053,16 @@ func (m *model) CurrentGlobalFile(folder string, file string) (protocol.FileInfo return f, ok, nil } +func (m *model) GetMtimeMapping(folder string, file string) (fs.MtimeMapping, error) { + m.fmut.RLock() + ffs, ok := m.folderFiles[folder] + m.fmut.RUnlock() + if !ok { + return fs.MtimeMapping{}, ErrFolderMissing + } + return fs.GetMtimeMapping(ffs.MtimeFS(), file) +} + // Connection returns the current connection for device, and a boolean whether a connection was found. func (m *model) Connection(deviceID protocol.DeviceID) (protocol.Connection, bool) { m.pmut.RLock()