sync: fix directory modtimes being set twice

When syncing to a backend which supports directory modtimes, a
directory which needed its modtime updated and which had files
transferred into it would get its modtime set twice - once when the
directory was checked and once in the pass at the end of the sync.

Now, when the end of sync pass is in use (which it is for all default
syncs), directories which need their modtime (or metadata) updating
are marked for that pass instead of being updated immediately. This
halves the number of directory modtime updates in a typical sync and
makes the "Updated dirs" stat count each directory once.
This commit is contained in:
Nick Craig-Wood
2026-09-05 12:14:46 +01:00
parent 6afb98b758
commit 351d70f694
2 changed files with 50 additions and 3 deletions
+15 -1
View File
@@ -1128,15 +1128,27 @@ func (s *syncCopyMove) copyDirMetadata(ctx context.Context, f fs.Fs, dst fs.Dire
newDst = dst
if !equal {
if s.setDirMetadata && s.copyEmptySrcDirs {
if dst != nil && s.setDirModTime && s.setDirModTimeAfter {
// Update the metadata in the delayed pass at the end of
// the sync in case transfers into the directory change it
s.markDirModified(dst.Remote())
} else {
newDst, err = operations.CopyDirMetadata(ctx, f, dst, dir, src)
}
} else if dst == nil && s.setDirModTime && s.copyEmptySrcDirs {
newDst, err = operations.MkdirModTime(ctx, f, dir, src.ModTime(ctx))
} else if dst == nil && s.copyEmptySrcDirs {
err = operations.Mkdir(ctx, f, dir)
} else if dst != nil && s.setDirModTime {
if s.setDirModTimeAfter {
// Set the modtime in the delayed pass at the end of the
// sync in case transfers into the directory change it
s.markDirModified(dst.Remote())
} else {
newDst, err = operations.SetDirModTime(ctx, f, dst, dir, src.ModTime(ctx))
}
}
}
if transform.Transforming(ctx) && newDst != nil && src.Remote() != newDst.Remote() {
s.markParentNotEmpty(src)
}
@@ -1197,7 +1209,9 @@ func (s *syncCopyMove) setDelayedDirModTimes(ctx context.Context) error {
continue
}
if !s.copyEmptySrcDirs {
if _, isEmpty := s.srcEmptyDirs[item.dir]; isEmpty {
// Skip empty source directories which were never
// created on the destination
if _, isEmpty := s.srcEmptyDirs[item.dir]; isEmpty && item.dst == nil {
continue
}
}
+33
View File
@@ -2868,6 +2868,39 @@ func TestSyncReplaceDirModTimeWithEmptyDirs(t *testing.T) {
testSyncReplaceDirModTime(t, true)
}
// Test that syncing a directory which needs its modtime updating and
// which has files transferred into it only sets its modtime once
func TestSyncSetDirModTimeOnce(t *testing.T) {
ctx := context.Background()
r := fstest.NewRun(t)
if r.Fremote.Features().DirSetModTime == nil {
t.Skip("Skipping test as remote does not support DirSetModTime")
}
file1 := r.WriteFile("dir/file1", "file1 contents", t2)
_, err := operations.SetDirModTime(ctx, r.Flocal, nil, "dir", t2)
require.NoError(t, err)
// Initial sync creates the directory and sets its modtime
accounting.GlobalStats().ResetCounters()
require.NoError(t, Sync(ctx, r.Fremote, r.Flocal, false))
r.CheckRemoteItems(t, file1)
assert.Equal(t, int64(1), accounting.GlobalStats().UpdatedDirs(0), "expected directory modtime to be set exactly once")
r.CheckDirectoryModTimes(t, "dir")
// Change the contents of the directory and its modtime
file1 = r.WriteFile("dir/file1", "file1 changed contents", t1)
_, err = operations.SetDirModTime(ctx, r.Flocal, nil, "dir", t1)
require.NoError(t, err)
// Sync again and check the modtime is only set once
accounting.GlobalStats().ResetCounters()
require.NoError(t, Sync(ctx, r.Fremote, r.Flocal, false))
r.CheckRemoteItems(t, file1)
assert.Equal(t, int64(1), accounting.GlobalStats().UpdatedDirs(0), "expected directory modtime to be set exactly once")
r.CheckDirectoryModTimes(t, "dir")
}
// Tests that nothing is transferred when src and dst already match
// Run the same sync twice, ensure no action is taken the second time
func testNothingToTransfer(t *testing.T, copyEmptySrcDirs bool) {