diff options
| author | tsne <tsne.dev@outlook.com> | 2026-07-06 16:52:20 +0200 |
|---|---|---|
| committer | tsne <tsne.dev@outlook.com> | 2026-07-06 21:37:42 +0200 |
| commit | 84da43998f7cc42cc72652333ac6e3d293abb23e (patch) | |
| tree | 6610f82df854ae86f7f34152bb05fc712ef6f314 /internal/cli/remote.go | |
| download | hopper-84da43998f7cc42cc72652333ac6e3d293abb23e.tar.gz | |
Diffstat (limited to 'internal/cli/remote.go')
| -rw-r--r-- | internal/cli/remote.go | 94 |
1 files changed, 94 insertions, 0 deletions
diff --git a/internal/cli/remote.go b/internal/cli/remote.go new file mode 100644 index 0000000..43fe93e --- /dev/null +++ b/internal/cli/remote.go @@ -0,0 +1,94 @@ +package cli + +import ( + "context" + "sort" + "strings" + "time" +) + +type remoteEntry struct { + path string + size int + lastChanged time.Time + isDir bool + deleted bool + + // orderKey is path with '/' replaced by 0x00, precomputed so the sort + // compares native strings. '/' and 0x00 are the only two bytes that can + // never appear in a path, and 0x00 sorts below every other byte, so this + // orders a directory's descendants ("a/...") as a contiguous run immediately + // after the directory ("a") and before any sibling that shares its base name + // ("a.js", "ab"). emptyDirs relies on that contiguity. + orderKey string +} + +// listRemoteEntries fetches all remote entries from the api. The returned list +// is sorted by orderKey (see remoteEntry.orderKey), which is the ordering +// emptyDirs requires. Callers must not re-sort it by path. +func listRemoteEntries(ctx context.Context, api storageAPI, prefix string) ([]remoteEntry, error) { + entries := make([]remoteEntry, 0, 64) + it := api.List(ctx, prefix) + for it.Next() { + o := it.Object() + entries = append(entries, remoteEntry{ + path: o.Path, + size: o.Size, + isDir: o.IsDirectory, + lastChanged: o.LastChanged, + orderKey: pathOrderKey(o.Path), + }) + } + if err := it.Err(); err != nil { + return nil, err + } + sort.Slice(entries, func(i, j int) bool { + return entries[i].orderKey < entries[j].orderKey + }) + return entries, nil +} + +// emptyDirs returns the directory entries whose subtree holds no surviving +// file. entries must be sorted by orderKey (see remoteEntry.orderKey), so a +// directory's subtree is the contiguous run of following entries sharing its +// path prefix. A directory exists only to hold files, so it survives iff some +// file beneath it survives. This makes the bottom-up cascade (a parent emptied +// by removing its last child directory) fall out without extra bookkeeping. +// A file survives unless it was deleted. +func emptyDirs(entries []remoteEntry) []*remoteEntry { + n := len(entries) + empty := make([]*remoteEntry, 0, n/4) + for i := 0; i < n; { + dir := &entries[i] + i++ + if !dir.isDir { + continue + } + + existingFiles := 0 + c := i + for prefix := dir.path + "/"; c < n; c++ { + child := &entries[c] + if !strings.HasPrefix(child.path, prefix) { + break + } + if !child.isDir && !child.deleted { + existingFiles++ + } + } + + if existingFiles == 0 { + // Emit only the top of the empty subtree and skip its descendants. + // Deleting a Bunny directory removes the subtree recursively, so + // deleting `dir` also removes any empty sub-dirs beneath it. + empty = append(empty, dir) + i = c + } + } + + return empty +} + +func pathOrderKey(path string) string { + return strings.ReplaceAll(path, "/", "\x00") +} |