Steps 12 & 12b: gRPC server and directory recursion + mirror.

Step 12: GardenSync gRPC server with 5 RPC handlers — PushManifest
(timestamp comparison, missing blob detection), PushBlobs (chunked
streaming, manifest replacement), PullManifest, PullBlobs, Prune.
Added store.List() and garden.ListBlobs()/DeleteBlob() for prune.
In-process tests via bufconn.

Step 12b: Add now recurses directories (walks files/symlinks, skips
dir entries). Mirror up syncs filesystem → manifest (add new, remove
deleted, rehash changed). Mirror down syncs manifest → filesystem
(restore + delete untracked with optional confirm). 7 tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-23 23:48:04 -07:00
parent 19217ec216
commit 0078b6b0f4
6 changed files with 614 additions and 14 deletions

View File

@@ -131,6 +131,32 @@ func (s *Store) Delete(hash string) error {
return nil
}
// List returns all blob hashes in the store by walking the blobs directory.
func (s *Store) List() ([]string, error) {
blobsDir := filepath.Join(s.root, "blobs")
var hashes []string
err := filepath.WalkDir(blobsDir, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
name := d.Name()
if validHash(name) {
hashes = append(hashes, name)
}
return nil
})
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, fmt.Errorf("store: listing blobs: %w", err)
}
return hashes, nil
}
// blobPath returns the filesystem path for a blob with the given hash.
// Layout: blobs/<first 2 hex chars>/<next 2 hex chars>/<full 64-char hash>
func (s *Store) blobPath(hash string) string {