diff --git a/cmd/bisync/bisync_test.go b/cmd/bisync/bisync_test.go index da89c3e20..f154d4d28 100644 --- a/cmd/bisync/bisync_test.go +++ b/cmd/bisync/bisync_test.go @@ -746,6 +746,16 @@ func (b *bisyncTest) runTestStep(ctx context.Context, line string) (err error) { case "test-func": b.TestFn = testFunc return + case "concurrent-func": + b.TestFn = func() { + src := filepath.Join(b.dataDir, "file7.txt") + dst := "file1.txt" + err := b.copyFile(ctx, src, b.replaceHex(b.path2), dst) + if err != nil { + fs.Errorf(src, "error copying file: %v", err) + } + } + return case "fix-names": // in case the local os converted any filenames ci.NoUnicodeNormalization = true diff --git a/cmd/bisync/deltas.go b/cmd/bisync/deltas.go index 1cb783d56..dde42e414 100644 --- a/cmd/bisync/deltas.go +++ b/cmd/bisync/deltas.go @@ -286,7 +286,7 @@ func (b *bisyncRun) findDeltas(fctx context.Context, f fs.Fs, oldListing string, } // applyDeltas -func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (changes1, changes2 bool, results2to1, results1to2 []Results, queues queues, err error) { +func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (results2to1, results1to2 []Results, queues queues, err error) { path1 := bilib.FsPath(b.fs1) path2 := bilib.FsPath(b.fs2) @@ -367,7 +367,7 @@ func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (change } } - //if there are potential conflicts to check, check them all here (outside the loop) in one fell swoop + // if there are potential conflicts to check, check them all here (outside the loop) in one fell swoop matches, err := b.checkconflicts(ctxCheck, filterCheck, b.fs1, b.fs2) for _, file := range ds1.sort() { @@ -392,7 +392,7 @@ func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (change } else if d2.is(deltaOther) { b.indent("!WARNING", file, "New or changed in both paths") - //if files are identical, leave them alone instead of renaming + // if files are identical, leave them alone instead of renaming if (dirs1.has(file) || dirs1.has(alias)) && (dirs2.has(file) || dirs2.has(alias)) { fs.Infof(nil, "This is a directory, not a file. Skipping equality check and will not rename: %s", file) ls1.getPut(file, skippedDirs1) @@ -486,7 +486,6 @@ func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (change // Do the batch operation if copy2to1.NotEmpty() && !b.InGracefulShutdown { - changes1 = true b.indent("Path2", "Path1", "Do queued copies to") ctx = b.setBackupDir(ctx, 1) results2to1, err = b.fastCopy(ctx, b.fs2, b.fs1, copy2to1, "copy2to1") @@ -498,12 +497,11 @@ func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (change return } - //copy empty dirs from path2 to path1 (if --create-empty-src-dirs) + // copy empty dirs from path2 to path1 (if --create-empty-src-dirs) b.syncEmptyDirs(ctx, b.fs1, copy2to1, dirs2, &results2to1, "make") } if copy1to2.NotEmpty() && !b.InGracefulShutdown { - changes2 = true b.indent("Path1", "Path2", "Do queued copies to") ctx = b.setBackupDir(ctx, 2) results1to2, err = b.fastCopy(ctx, b.fs1, b.fs2, copy1to2, "copy1to2") @@ -515,7 +513,7 @@ func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (change return } - //copy empty dirs from path1 to path2 (if --create-empty-src-dirs) + // copy empty dirs from path1 to path2 (if --create-empty-src-dirs) b.syncEmptyDirs(ctx, b.fs2, copy1to2, dirs1, &results1to2, "make") } @@ -523,7 +521,7 @@ func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (change if err = b.saveQueue(delete1, "delete1"); err != nil { return } - //propagate deletions of empty dirs from path2 to path1 (if --create-empty-src-dirs) + // propagate deletions of empty dirs from path2 to path1 (if --create-empty-src-dirs) b.syncEmptyDirs(ctx, b.fs1, delete1, dirs1, &results2to1, "remove") } @@ -531,7 +529,7 @@ func (b *bisyncRun) applyDeltas(ctx context.Context, ds1, ds2 *deltaSet) (change if err = b.saveQueue(delete2, "delete2"); err != nil { return } - //propagate deletions of empty dirs from path1 to path2 (if --create-empty-src-dirs) + // propagate deletions of empty dirs from path1 to path2 (if --create-empty-src-dirs) b.syncEmptyDirs(ctx, b.fs2, delete2, dirs2, &results1to2, "remove") } diff --git a/cmd/bisync/operations.go b/cmd/bisync/operations.go index 6a1d9d1fa..75b36e8b0 100644 --- a/cmd/bisync/operations.go +++ b/cmd/bisync/operations.go @@ -359,8 +359,6 @@ func (b *bisyncRun) runLocked(octx context.Context) (err error) { // Determine and apply changes to Path1 and Path2 noChanges := ds1.empty() && ds2.empty() - changes1 := false // 2to1 - changes2 := false // 1to2 results2to1 := []Results{} results1to2 := []Results{} @@ -370,7 +368,7 @@ func (b *bisyncRun) runLocked(octx context.Context) (err error) { fs.Infof(nil, "No changes found") } else { fs.Infof(nil, "Applying changes") - changes1, changes2, results2to1, results1to2, queues, err = b.applyDeltas(octx, ds1, ds2) + results2to1, results1to2, queues, err = b.applyDeltas(octx, ds1, ds2) if err != nil { if b.InGracefulShutdown && (err == context.Canceled || err == accounting.ErrorMaxTransferLimitReachedGraceful || strings.Contains(err.Error(), "context canceled")) { fs.Infof(nil, "Ignoring sync error due to Graceful Shutdown: %v", err) @@ -395,21 +393,11 @@ func (b *bisyncRun) runLocked(octx context.Context) (err error) { } b.saveOldListings() // save new listings - // NOTE: "changes" in this case does not mean this run vs. last run, it means start of this run vs. end of this run. - // i.e. whether we can use the March lst-new as this side's lst without modifying it. if noChanges { b.replaceCurrentListings() } else { - if changes1 || b.InGracefulShutdown { // 2to1 - err1 = b.modifyListing(fctx, b.fs2, b.fs1, results2to1, queues, false) - } else { - err1 = bilib.CopyFileIfExists(b.newListing1, b.listing1) - } - if changes2 || b.InGracefulShutdown { // 1to2 - err2 = b.modifyListing(fctx, b.fs1, b.fs2, results1to2, queues, true) - } else { - err2 = bilib.CopyFileIfExists(b.newListing2, b.listing2) - } + err1 = b.modifyListing(fctx, b.fs2, b.fs1, results2to1, queues, false) // 2to1 + err2 = b.modifyListing(fctx, b.fs1, b.fs2, results1to2, queues, true) // 1to2 } if b.DebugName != "" { l1, _ := b.loadListing(b.listing1) diff --git a/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.copy2to1.que b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.copy2to1.que new file mode 100644 index 000000000..029f3e190 --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.copy2to1.que @@ -0,0 +1 @@ +"file1.txt" diff --git a/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst new file mode 100644 index 000000000..021dde2dc --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst @@ -0,0 +1,10 @@ +# bisync listing v1 from test +- 109 - - 2000-01-01T00:00:00.000000000+0000 "RCLONE_TEST" +- 19 - - 2023-08-26T00:00:00.000000000+0000 "file1.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file2.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file3.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file4.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file5.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file6.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file7.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file8.txt" diff --git a/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst-new b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst-new new file mode 100644 index 000000000..021dde2dc --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst-new @@ -0,0 +1,10 @@ +# bisync listing v1 from test +- 109 - - 2000-01-01T00:00:00.000000000+0000 "RCLONE_TEST" +- 19 - - 2023-08-26T00:00:00.000000000+0000 "file1.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file2.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file3.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file4.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file5.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file6.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file7.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file8.txt" diff --git a/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst-old b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst-old new file mode 100644 index 000000000..021dde2dc --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path1.lst-old @@ -0,0 +1,10 @@ +# bisync listing v1 from test +- 109 - - 2000-01-01T00:00:00.000000000+0000 "RCLONE_TEST" +- 19 - - 2023-08-26T00:00:00.000000000+0000 "file1.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file2.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file3.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file4.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file5.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file6.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file7.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file8.txt" diff --git a/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst new file mode 100644 index 000000000..021dde2dc --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst @@ -0,0 +1,10 @@ +# bisync listing v1 from test +- 109 - - 2000-01-01T00:00:00.000000000+0000 "RCLONE_TEST" +- 19 - - 2023-08-26T00:00:00.000000000+0000 "file1.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file2.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file3.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file4.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file5.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file6.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file7.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file8.txt" diff --git a/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst-new b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst-new new file mode 100644 index 000000000..021dde2dc --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst-new @@ -0,0 +1,10 @@ +# bisync listing v1 from test +- 109 - - 2000-01-01T00:00:00.000000000+0000 "RCLONE_TEST" +- 19 - - 2023-08-26T00:00:00.000000000+0000 "file1.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file2.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file3.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file4.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file5.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file6.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file7.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file8.txt" diff --git a/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst-old b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst-old new file mode 100644 index 000000000..021dde2dc --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/golden/_testdir_path1.._testdir_path2.path2.lst-old @@ -0,0 +1,10 @@ +# bisync listing v1 from test +- 109 - - 2000-01-01T00:00:00.000000000+0000 "RCLONE_TEST" +- 19 - - 2023-08-26T00:00:00.000000000+0000 "file1.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file2.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file3.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file4.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file5.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file6.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file7.txt" +- 0 - - 2000-01-01T00:00:00.000000000+0000 "file8.txt" diff --git a/cmd/bisync/testdata/test_concurrent/golden/test.log b/cmd/bisync/testdata/test_concurrent/golden/test.log new file mode 100644 index 000000000..35f894aed --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/golden/test.log @@ -0,0 +1,73 @@ +(01) : test concurrent + +(02) : test initial bisync +(03) : bisync resync +INFO : Setting --ignore-listing-checksum as neither --checksum nor --compare checksum are set. +INFO : Bisyncing with Comparison Settings: +{ +"Modtime": true, +"Size": true, +"Checksum": false, +"NoSlowHash": false, +"SlowHashSyncOnly": false, +"DownloadHash": false +} +INFO : Synching Path1 "{path1/}" with Path2 "{path2/}" +INFO : Copying Path2 files to Path1 +INFO : - Path2 Resync is copying files to - Path1 +INFO : - Path1 Resync is copying files to - Path2 +INFO : Resync updating listings +INFO : Validating listings for Path1 "{path1/}" vs Path2 "{path2/}" +INFO : Bisync successful + +(04) : test changed on one path - file1 +(05) : touch-glob 2001-01-02 {datadir/} file5R.txt +(06) : touch-glob 2023-08-26 {datadir/} file7.txt +(07) : copy-as {datadir/}file5R.txt {path2/} file1.txt + +(08) : test bisync with file changed during +(09) : concurrent-func +(10) : bisync +INFO : Setting --ignore-listing-checksum as neither --checksum nor --compare checksum are set. +INFO : Bisyncing with Comparison Settings: +{ +"Modtime": true, +"Size": true, +"Checksum": false, +"NoSlowHash": false, +"SlowHashSyncOnly": false, +"DownloadHash": false +} +INFO : Synching Path1 "{path1/}" with Path2 "{path2/}" +INFO : Building Path1 and Path2 listings +INFO : Path1 checking for diffs +INFO : Path2 checking for diffs +INFO : - Path2 File changed: size (larger), time (newer) - file1.txt +INFO : Path2: 1 changes:  0 new,  1 modified,  0 deleted +INFO : (Modified:  1 newer,  0 older,  1 larger,  0 smaller) +INFO : Applying changes +INFO : - Path2 Queue copy to Path1 - {path1/}file1.txt +INFO : - Path2 Do queued copies to - Path1 +INFO : Updating listings +INFO : Validating listings for Path1 "{path1/}" vs Path2 "{path2/}" +INFO : Bisync successful + +(11) : bisync +INFO : Setting --ignore-listing-checksum as neither --checksum nor --compare checksum are set. +INFO : Bisyncing with Comparison Settings: +{ +"Modtime": true, +"Size": true, +"Checksum": false, +"NoSlowHash": false, +"SlowHashSyncOnly": false, +"DownloadHash": false +} +INFO : Synching Path1 "{path1/}" with Path2 "{path2/}" +INFO : Building Path1 and Path2 listings +INFO : Path1 checking for diffs +INFO : Path2 checking for diffs +INFO : No changes found +INFO : Updating listings +INFO : Validating listings for Path1 "{path1/}" vs Path2 "{path2/}" +INFO : Bisync successful diff --git a/cmd/bisync/testdata/test_concurrent/initial/RCLONE_TEST b/cmd/bisync/testdata/test_concurrent/initial/RCLONE_TEST new file mode 100644 index 000000000..d8ca97c2a --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/initial/RCLONE_TEST @@ -0,0 +1 @@ +This file is used for testing the health of rclone accesses to the local/remote file system. Do not delete. diff --git a/cmd/bisync/testdata/test_concurrent/initial/file1.txt b/cmd/bisync/testdata/test_concurrent/initial/file1.txt new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/bisync/testdata/test_concurrent/initial/file2.txt b/cmd/bisync/testdata/test_concurrent/initial/file2.txt new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/bisync/testdata/test_concurrent/initial/file3.txt b/cmd/bisync/testdata/test_concurrent/initial/file3.txt new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/bisync/testdata/test_concurrent/initial/file4.txt b/cmd/bisync/testdata/test_concurrent/initial/file4.txt new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/bisync/testdata/test_concurrent/initial/file5.txt b/cmd/bisync/testdata/test_concurrent/initial/file5.txt new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/bisync/testdata/test_concurrent/initial/file6.txt b/cmd/bisync/testdata/test_concurrent/initial/file6.txt new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/bisync/testdata/test_concurrent/initial/file7.txt b/cmd/bisync/testdata/test_concurrent/initial/file7.txt new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/bisync/testdata/test_concurrent/initial/file8.txt b/cmd/bisync/testdata/test_concurrent/initial/file8.txt new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/dummy.txt b/cmd/bisync/testdata/test_concurrent/modfiles/dummy.txt new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/file1.txt b/cmd/bisync/testdata/test_concurrent/modfiles/file1.txt new file mode 100644 index 000000000..464147f09 --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/modfiles/file1.txt @@ -0,0 +1 @@ +This file is newer diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/file10.txt b/cmd/bisync/testdata/test_concurrent/modfiles/file10.txt new file mode 100644 index 000000000..464147f09 --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/modfiles/file10.txt @@ -0,0 +1 @@ +This file is newer diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/file11.txt b/cmd/bisync/testdata/test_concurrent/modfiles/file11.txt new file mode 100644 index 000000000..464147f09 --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/modfiles/file11.txt @@ -0,0 +1 @@ +This file is newer diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/file2.txt b/cmd/bisync/testdata/test_concurrent/modfiles/file2.txt new file mode 100644 index 000000000..0fd70321a --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/modfiles/file2.txt @@ -0,0 +1 @@ +Newer version \ No newline at end of file diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/file5L.txt b/cmd/bisync/testdata/test_concurrent/modfiles/file5L.txt new file mode 100644 index 000000000..43ceff1db --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/modfiles/file5L.txt @@ -0,0 +1 @@ +This file is newer and not equal to 5R diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/file5R.txt b/cmd/bisync/testdata/test_concurrent/modfiles/file5R.txt new file mode 100644 index 000000000..a928fcf13 --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/modfiles/file5R.txt @@ -0,0 +1 @@ +This file is newer and not equal to 5L diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/file6.txt b/cmd/bisync/testdata/test_concurrent/modfiles/file6.txt new file mode 100644 index 000000000..464147f09 --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/modfiles/file6.txt @@ -0,0 +1 @@ +This file is newer diff --git a/cmd/bisync/testdata/test_concurrent/modfiles/file7.txt b/cmd/bisync/testdata/test_concurrent/modfiles/file7.txt new file mode 100644 index 000000000..464147f09 --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/modfiles/file7.txt @@ -0,0 +1 @@ +This file is newer diff --git a/cmd/bisync/testdata/test_concurrent/scenario.txt b/cmd/bisync/testdata/test_concurrent/scenario.txt new file mode 100644 index 000000000..19ca9fcc2 --- /dev/null +++ b/cmd/bisync/testdata/test_concurrent/scenario.txt @@ -0,0 +1,15 @@ +test concurrent + +test initial bisync +bisync resync + +test changed on one path - file1 +touch-glob 2001-01-02 {datadir/} file5R.txt +touch-glob 2023-08-26 {datadir/} file7.txt +copy-as {datadir/}file5R.txt {path2/} file1.txt + +test bisync with file changed during +concurrent-func +bisync + +bisync \ No newline at end of file diff --git a/docs/content/bisync.md b/docs/content/bisync.md index 458f92eab..3aa0bae54 100644 --- a/docs/content/bisync.md +++ b/docs/content/bisync.md @@ -1815,6 +1815,9 @@ about _Unison_ and synchronization in general. ## Changelog +### `v1.69.1` +* Fixed an issue causing listings to not capture concurrent modifications under certain conditions + ### `v1.68` * Fixed an issue affecting backends that round modtimes to a lower precision.