chore: slightly optimise rename detection (ref #10777) (#10819)

Only run rename detection for new files. This skips an expensive check
for all updates to existing files. The tradeoff is that we no longer
immediately detect renames on top of another file as a rename -- this
may instead become a copy+delete operation on the destination.

---------

Signed-off-by: Jakob Borg <jakob@kastelo.net>
This commit is contained in:
Jakob Borg
2026-07-25 07:46:56 +00:00
committed by GitHub
parent b15ff42d52
commit ec35a95318
4 changed files with 33 additions and 14 deletions
+13 -6
View File
@@ -745,12 +745,19 @@ func (f *folder) scanSubdirsChangedAndNew(ctx context.Context, subDirs []string,
switch f.Type { switch f.Type {
case config.FolderTypeReceiveOnly, config.FolderTypeReceiveEncrypted: case config.FolderTypeReceiveOnly, config.FolderTypeReceiveEncrypted:
default: default:
if nf, ok := f.findRename(ctx, res.File, batch); ok { // Rename detection is comparatively expensive, so only attempt
if ok, err := batch.Update(nf); err != nil { // it for files that appeared as new on disk during this scan. A
return 0, err // rename that overwrites an existing file (the destination path
} else if ok { // already had an entry, so it scans as an update rather than a
changes++ // new file) is not optimised as a rename.
batch.markDeleted(nf.Name) if res.File.New && res.File.Size > 0 {
if nf, ok := f.findRename(ctx, res.File, batch); ok {
if ok, err := batch.Update(nf); err != nil {
return 0, err
} else if ok {
changes++
batch.markDeleted(nf.Name)
}
} }
} }
} }
+10 -7
View File
@@ -3270,17 +3270,20 @@ func TestRenameSequenceOrder(t *testing.T) {
t.Errorf("Unexpected count: %d != %d", count, numFiles) t.Errorf("Unexpected count: %d != %d", count, numFiles)
} }
// Modify all the files, other than the ones we expect to rename // Modify all the files other than the rename sources, whose content we
// keep intact so the renamed copies still match by block hash.
for i := 0; i < numFiles; i++ { for i := 0; i < numFiles; i++ {
if i == 3 || i == 17 || i == 16 || i == 4 { if i == 3 || i == 16 {
continue continue
} }
v := fmt.Sprintf("%d", i) v := fmt.Sprintf("%d", i)
writeFile(t, ffs, v, []byte(v+"-new")) writeFile(t, ffs, v, []byte(v+"-new"))
} }
// Rename // Rename to previously unseen names. Renaming onto an existing name is
must(t, ffs.Rename("3", "17")) // treated as an in-place update rather than a rename, since rename
must(t, ffs.Rename("16", "4")) // detection only runs for files that are new on disk.
must(t, ffs.Rename("3", "20"))
must(t, ffs.Rename("16", "21"))
// Scan // Scan
m.ScanFolders() m.ScanFolders()
@@ -3291,10 +3294,10 @@ func TestRenameSequenceOrder(t *testing.T) {
it, errFn := m.LocalFilesSequenced("default", protocol.LocalDeviceID, 0) it, errFn := m.LocalFilesSequenced("default", protocol.LocalDeviceID, 0)
for i := range it { for i := range it {
t.Log(i) t.Log(i)
if i.FileName() == "17" { if i.FileName() == "20" {
firstExpectedSequence = i.SequenceNo() + 1 firstExpectedSequence = i.SequenceNo() + 1
} }
if i.FileName() == "4" { if i.FileName() == "21" {
secondExpectedSequence = i.SequenceNo() + 1 secondExpectedSequence = i.SequenceNo() + 1
} }
if i.FileName() == "3" { if i.FileName() == "3" {
+4 -1
View File
@@ -146,6 +146,8 @@ type FileInfo struct {
Deleted bool Deleted bool
NoPermissions bool NoPermissions bool
New bool // set by the scanner when the file newly appeared on disk; no db / wire representation
truncated bool // was created from a truncated file info without blocks truncated bool // was created from a truncated file info without blocks
} }
@@ -233,7 +235,8 @@ func (f *FileInfo) LogAttr() slog.Attr {
case FileInfoTypeFile: case FileInfoTypeFile:
kind = "file" kind = "file"
if !f.Deleted { if !f.Deleted {
attrs = append(attrs, attrs = append(
attrs,
slog.Any("modified", f.ModTime()), slog.Any("modified", f.ModTime()),
slog.String("permissions", fmt.Sprintf("0%03o", f.Permissions)), slog.String("permissions", fmt.Sprintf("0%03o", f.Permissions)),
slog.Int64("size", f.Size), slog.Int64("size", f.Size),
+6
View File
@@ -471,6 +471,8 @@ func (w *walker) walkRegular(ctx context.Context, relPath string, info fs.FileIn
f.RawBlockSize = int32(blockSize) f.RawBlockSize = int32(blockSize)
l.Debugln(w, "checking:", f) l.Debugln(w, "checking:", f)
f.New = !hasCurFile
if hasCurFile { if hasCurFile {
if curFile.IsEquivalentOptional(f, protocol.FileInfoComparison{ if curFile.IsEquivalentOptional(f, protocol.FileInfoComparison{
ModTimeWindow: w.ModTimeWindow, ModTimeWindow: w.ModTimeWindow,
@@ -518,6 +520,8 @@ func (w *walker) walkDir(ctx context.Context, relPath string, info fs.FileInfo,
f.NoPermissions = w.IgnorePerms f.NoPermissions = w.IgnorePerms
l.Debugln(w, "checking:", f) l.Debugln(w, "checking:", f)
f.New = !hasCurFile
if hasCurFile { if hasCurFile {
if curFile.IsEquivalentOptional(f, protocol.FileInfoComparison{ if curFile.IsEquivalentOptional(f, protocol.FileInfoComparison{
ModTimeWindow: w.ModTimeWindow, ModTimeWindow: w.ModTimeWindow,
@@ -570,6 +574,8 @@ func (w *walker) walkSymlink(ctx context.Context, relPath string, info fs.FileIn
f = w.updateFileInfo(f, curFile) f = w.updateFileInfo(f, curFile)
l.Debugln(w, "checking:", f) l.Debugln(w, "checking:", f)
f.New = !hasCurFile
if hasCurFile { if hasCurFile {
if curFile.IsEquivalentOptional(f, protocol.FileInfoComparison{ if curFile.IsEquivalentOptional(f, protocol.FileInfoComparison{
ModTimeWindow: w.ModTimeWindow, ModTimeWindow: w.ModTimeWindow,