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
This commit is contained in:
Chris Lu
2026-09-02 11:45:22 -07:00
committed by GitHub
parent 9ea52db219
commit 23a6b8feb5
2 changed files with 44 additions and 5 deletions
+5 -5
View File
@@ -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) { func TraverseBfs(ctx context.Context, filerClient FilerClient, parentPath util.FullPath, fn func(parentPath util.FullPath, entry *Entry) error) (err error) {
K := 5 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) ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
@@ -102,11 +106,7 @@ func processOneDirectory(ctx context.Context, filerClient FilerClient, parentPat
} }
if entry.IsDirectory { if entry.IsDirectory {
subDir := fmt.Sprintf("%s/%s", parentPath, entry.Name) if !enqueue(parentPath.Child(entry.Name)) {
if parentPath == "/" {
subDir = "/" + entry.Name
}
if !enqueue(util.FullPath(subDir)) {
return ctx.Err() return ctx.Err()
} }
} }
+39
View File
@@ -4,6 +4,8 @@ import (
"context" "context"
"fmt" "fmt"
"net" "net"
"reflect"
"sort"
"strings" "strings"
"sync" "sync"
"testing" "testing"
@@ -150,3 +152,40 @@ func TestReadDirAllEntriesStuckPagination(t *testing.T) {
t.Fatal("listing loops forever on a non-advancing store") 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)
}
}