From 1637941368b3f0497a473591abae4d993c0e7361 Mon Sep 17 00:00:00 2001 From: Jiri Puc Date: Mon, 2 Nov 2026 19:09:22 +0100 Subject: [PATCH] filer: allow renaming into a sibling that shares the name prefix (#12337) CanRename rejected a move whenever the new parent directory's path started with the source path as a plain string, so moving /dir/a into /dir/a.d (or anything under /dir/ab) failed with "can not move directory to a subdirectory of itself". Check the path boundary instead: reject only when the new parent is the source itself or lies under it. Claude-Session: https://claude.ai/code/session_01VncHKTehVroSAzRMUHykEX Co-authored-by: Claude Opus 5.5 --- weed/filer/filer_rename.go | 9 ++++++++- weed/filer/filer_rename_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/weed/filer/filer_rename.go b/weed/filer/filer_rename.go index b39d9872e1..1a9c48fc59 100644 --- a/weed/filer/filer_rename.go +++ b/weed/filer/filer_rename.go @@ -10,7 +10,7 @@ import ( func (f *Filer) CanRename(ctx context.Context, source, target util.FullPath, oldName string) error { sourcePath := source.Child(oldName) - if strings.HasPrefix(string(target), string(sourcePath)) { + if movesIntoItself(sourcePath, target) { return fmt.Errorf("mv: can not move directory to a subdirectory of itself") } @@ -33,6 +33,13 @@ func (f *Filer) CanRename(ctx context.Context, source, target util.FullPath, old return nil } +// movesIntoItself reports whether target, the new parent directory, is +// sourcePath or lies under it. A sibling that only shares a name prefix, +// such as /a.d for /a, does not. +func movesIntoItself(sourcePath, target util.FullPath) bool { + return target == sourcePath || target.IsUnder(sourcePath) +} + func (f *Filer) DetectBucket(source util.FullPath) (bucket string) { if strings.HasPrefix(string(source), f.DirBucketsPath+"/") { bucketAndObjectKey := string(source)[len(f.DirBucketsPath)+1:] diff --git a/weed/filer/filer_rename_test.go b/weed/filer/filer_rename_test.go index d7096963fa..5df9429c14 100644 --- a/weed/filer/filer_rename_test.go +++ b/weed/filer/filer_rename_test.go @@ -29,3 +29,27 @@ func TestDetectBucket(t *testing.T) { }) } } + +func TestMovesIntoItself(t *testing.T) { + tests := []struct { + name string + sourcePath string + target string + want bool + }{ + {"into itself", "/dir/a", "/dir/a", true}, + {"into a child", "/dir/a", "/dir/a/b", true}, + {"into a deeper descendant", "/dir/a", "/dir/a/b/c", true}, + {"into a sibling sharing the name prefix", "/dir/a", "/dir/a.d", false}, + {"into a descendant of that sibling", "/dir/a", "/dir/ab/c", false}, + {"into the parent", "/dir/a", "/dir", false}, + {"into an unrelated directory", "/dir/a", "/other", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := movesIntoItself(util.FullPath(tt.sourcePath), util.FullPath(tt.target)); got != tt.want { + t.Errorf("movesIntoItself(%q, %q) = %v, want %v", tt.sourcePath, tt.target, got, tt.want) + } + }) + } +}