From 387be8d8c03a4340a7547d26fd7559a4af5f51ec Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Thu, 28 May 2026 15:45:40 +0100 Subject: [PATCH] drime: fix directory rename leaving the renamed folder empty in VFS Drime's folder rename returns success immediately but the children listing of the renamed folder briefly returns empty before settling, which made VFS see an empty directory right after a successful rename and broke subsequent operations on its contents. After a successful DirMove, poll the renamed folder's listing until it shows the pre-rename child count, with a 30s timeout and exponential backoff. The backend integration test already tolerated this via its own list-consistency retries; this brings VFS into line. Fixes #9450 --- backend/drime/drime.go | 58 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/backend/drime/drime.go b/backend/drime/drime.go index ee5e55ca3..2499627f6 100644 --- a/backend/drime/drime.go +++ b/backend/drime/drime.go @@ -927,6 +927,48 @@ func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, return dstObj, nil } +// countDirChildren returns the number of direct children of dirID. +func (f *Fs) countDirChildren(ctx context.Context, dirID string) (int, error) { + n := 0 + _, err := f.listAll(ctx, dirID, false, false, "", func(*api.Item) bool { + n++ + return false + }) + return n, err +} + +// waitForDirChildren polls dirID until it lists at least want direct +// children, or the timeout elapses. Drime's folder rename has visible +// eventual-consistency on the children listing - the rename returns +// success immediately but the renamed folder lists as empty for a +// short window - so callers that rename a folder use this to wait for +// the listing to settle before returning (see issue #9450). +func (f *Fs) waitForDirChildren(ctx context.Context, dirID string, want int) error { + const timeout = 30 * time.Second + deadline := time.Now().Add(timeout) + backoff := 100 * time.Millisecond + for { + got, err := f.countDirChildren(ctx, dirID) + if err != nil { + return err + } + if got >= want { + return nil + } + if time.Now().After(deadline) { + return fmt.Errorf("timed out waiting for directory listing to settle: got %d, want %d", got, want) + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(backoff): + } + if backoff < 2*time.Second { + backoff *= 2 + } + } +} + // DirMove moves src, srcRemote to this remote at dstRemote // using server-side move operations. // @@ -947,11 +989,25 @@ func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string return err } + // Count children before the move so we can wait for the server's + // children listing to reflect the rename - see waitForDirChildren. + preCount, err := f.countDirChildren(ctx, srcID) + if err != nil { + return fmt.Errorf("DirMove: count source children: %w", err) + } + // Do the move - _, err = f.moveTo(ctx, srcID, srcLeaf, dstLeaf, srcDirectoryID, dstDirectoryID) + info, err := f.moveTo(ctx, srcID, srcLeaf, dstLeaf, srcDirectoryID, dstDirectoryID) if err != nil { return err } + + if preCount > 0 { + if err := f.waitForDirChildren(ctx, info.ID.String(), preCount); err != nil { + fs.Logf(f, "DirMove: %v (continuing)", err) + } + } + srcFs.dirCache.FlushDir(srcRemote) return nil }