From 23a6b8feb5480b65cdcbaf5771a771a7cd7aac06 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Wed, 2 Sep 2026 11:45:22 -0700 Subject: [PATCH] filer_pb: walk the whole tree when the BFS start path ends in a slash (#11099) * filer_pb: build BFS child paths with FullPath.Child A start path with a trailing slash produced "/dir//sub" for every subdirectory, and the filer only trims a trailing slash, so those listings came back empty and the walk stopped after the first level. Claude-Session: https://claude.ai/code/session_01Jp9tXRpBv9gvh8fkaVFqxQ * filer_pb: normalize the BFS start path Entries directly under the start path were reported with the caller's trailing slash, so filer.meta.backup wrote them under a directory the incremental stream never names again. Claude-Session: https://claude.ai/code/session_01Jp9tXRpBv9gvh8fkaVFqxQ --- weed/pb/filer_pb/filer_client_bfs.go | 10 +++--- weed/pb/filer_pb/filer_client_bfs_test.go | 39 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/weed/pb/filer_pb/filer_client_bfs.go b/weed/pb/filer_pb/filer_client_bfs.go index 451b58511..e4ec78d67 100644 --- a/weed/pb/filer_pb/filer_client_bfs.go +++ b/weed/pb/filer_pb/filer_client_bfs.go @@ -15,6 +15,10 @@ import ( func TraverseBfs(ctx context.Context, filerClient FilerClient, parentPath util.FullPath, fn func(parentPath util.FullPath, entry *Entry) error) (err error) { K := 5 + // callers hand in user-typed paths, and every entry is reported relative + // to this one, so a trailing slash must not reach the callback + parentPath = util.NormalizePath(string(parentPath)) + ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -102,11 +106,7 @@ func processOneDirectory(ctx context.Context, filerClient FilerClient, parentPat } if entry.IsDirectory { - subDir := fmt.Sprintf("%s/%s", parentPath, entry.Name) - if parentPath == "/" { - subDir = "/" + entry.Name - } - if !enqueue(util.FullPath(subDir)) { + if !enqueue(parentPath.Child(entry.Name)) { return ctx.Err() } } diff --git a/weed/pb/filer_pb/filer_client_bfs_test.go b/weed/pb/filer_pb/filer_client_bfs_test.go index c9ec79e14..2395623df 100644 --- a/weed/pb/filer_pb/filer_client_bfs_test.go +++ b/weed/pb/filer_pb/filer_client_bfs_test.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "net" + "reflect" + "sort" "strings" "sync" "testing" @@ -150,3 +152,40 @@ func TestReadDirAllEntriesStuckPagination(t *testing.T) { t.Fatal("listing loops forever on a non-advancing store") } } + +// A start path with a trailing slash must still descend into subdirectories. +func TestTraverseBfsTrailingSlashRoot(t *testing.T) { + server := &stubFiler{} + server.listings = func(req *ListEntriesRequest, send func(*Entry) error) error { + if req.StartFromFileName != "" { + return nil + } + // the filer trims a single trailing slash, nothing more + switch strings.TrimSuffix(req.Directory, "/") { + case "/buckets/data": + return send(&Entry{Name: "sub", IsDirectory: true}) + case "/buckets/data/sub": + return send(&Entry{Name: "a.txt"}) + } + return nil + } + filerClient := startStubFiler(t, server) + + var mu sync.Mutex + var seen []string + err := TraverseBfs(context.Background(), filerClient, "/buckets/data/", func(parentPath util.FullPath, entry *Entry) error { + mu.Lock() + seen = append(seen, fmt.Sprintf("%s -> %s", parentPath, entry.Name)) + mu.Unlock() + return nil + }) + if err != nil { + t.Fatal(err) + } + + sort.Strings(seen) + want := []string{"/buckets/data -> sub", "/buckets/data/sub -> a.txt"} + if !reflect.DeepEqual(seen, want) { + t.Fatalf("visited %v, want %v", seen, want) + } +}