Step 15: CLI wiring, prune, and sgardd daemon.
Local prune: garden.Prune() removes orphaned blobs. 2 tests. CLI commands: sgard push, sgard pull (with SSH auth via --ssh-key or ssh-agent), sgard prune (local by default, remote with --remote). Server daemon: cmd/sgardd with --listen, --repo, --authorized-keys flags. Runs gRPC server with optional SSH key auth interceptor. Root command gains --remote and --ssh-key persistent flags with resolveRemote() (flag > env > repo/remote file). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
31
garden/prune.go
Normal file
31
garden/prune.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package garden
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Prune removes orphaned blobs that are not referenced by any manifest entry.
|
||||
// Returns the number of blobs removed.
|
||||
func (g *Garden) Prune() (int, error) {
|
||||
referenced := make(map[string]bool)
|
||||
for _, e := range g.manifest.Files {
|
||||
if e.Type == "file" && e.Hash != "" {
|
||||
referenced[e.Hash] = true
|
||||
}
|
||||
}
|
||||
|
||||
allBlobs, err := g.store.List()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("listing blobs: %w", err)
|
||||
}
|
||||
|
||||
removed := 0
|
||||
for _, hash := range allBlobs {
|
||||
if !referenced[hash] {
|
||||
if err := g.store.Delete(hash); err != nil {
|
||||
return removed, fmt.Errorf("deleting blob %s: %w", hash, err)
|
||||
}
|
||||
removed++
|
||||
}
|
||||
}
|
||||
|
||||
return removed, nil
|
||||
}
|
||||
79
garden/prune_test.go
Normal file
79
garden/prune_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package garden
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPruneRemovesOrphanedBlob(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
repoDir := filepath.Join(root, "repo")
|
||||
|
||||
g, err := Init(repoDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
|
||||
// Add a file, then remove it from manifest. The blob becomes orphaned.
|
||||
testFile := filepath.Join(root, "testfile")
|
||||
if err := os.WriteFile(testFile, []byte("orphan data"), 0o644); err != nil {
|
||||
t.Fatalf("writing test file: %v", err)
|
||||
}
|
||||
|
||||
if err := g.Add([]string{testFile}); err != nil {
|
||||
t.Fatalf("Add: %v", err)
|
||||
}
|
||||
|
||||
hash := g.manifest.Files[0].Hash
|
||||
if !g.BlobExists(hash) {
|
||||
t.Fatal("blob should exist before prune")
|
||||
}
|
||||
|
||||
if err := g.Remove([]string{testFile}); err != nil {
|
||||
t.Fatalf("Remove: %v", err)
|
||||
}
|
||||
|
||||
removed, err := g.Prune()
|
||||
if err != nil {
|
||||
t.Fatalf("Prune: %v", err)
|
||||
}
|
||||
if removed != 1 {
|
||||
t.Errorf("removed %d blobs, want 1", removed)
|
||||
}
|
||||
if g.BlobExists(hash) {
|
||||
t.Error("orphaned blob should be deleted after prune")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPruneKeepsReferencedBlobs(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
repoDir := filepath.Join(root, "repo")
|
||||
|
||||
g, err := Init(repoDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Init: %v", err)
|
||||
}
|
||||
|
||||
testFile := filepath.Join(root, "testfile")
|
||||
if err := os.WriteFile(testFile, []byte("keep me"), 0o644); err != nil {
|
||||
t.Fatalf("writing test file: %v", err)
|
||||
}
|
||||
|
||||
if err := g.Add([]string{testFile}); err != nil {
|
||||
t.Fatalf("Add: %v", err)
|
||||
}
|
||||
|
||||
hash := g.manifest.Files[0].Hash
|
||||
|
||||
removed, err := g.Prune()
|
||||
if err != nil {
|
||||
t.Fatalf("Prune: %v", err)
|
||||
}
|
||||
if removed != 0 {
|
||||
t.Errorf("removed %d blobs, want 0 (all referenced)", removed)
|
||||
}
|
||||
if !g.BlobExists(hash) {
|
||||
t.Error("referenced blob should still exist after prune")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user