Address all four review concerns on PR #31: 1. Fix missed bare fmt.Println() in VerifySnapshotWithOptions (line 620) 2. Replace all direct fmt.Fprintf(v.Stdout,...) / fmt.Fprintln(v.Stdout,...) / fmt.Fscanln(v.Stdin,...) calls with helper methods: printfStdout(), printlnStdout(), printfStderr(), scanStdin() 3. Route progress bar and stderr output through v.Stderr instead of os.Stderr in restore.go (concern #4: v.Stderr now actually used) 4. Rename exported Outputf to unexported printfStdout (YAGNI: only helpers actually used are created)
204 lines
5.6 KiB
Go
204 lines
5.6 KiB
Go
package vaultik
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.eeqj.de/sneak/vaultik/internal/log"
|
|
"github.com/dustin/go-humanize"
|
|
)
|
|
|
|
// PruneOptions contains options for the prune command
|
|
type PruneOptions struct {
|
|
Force bool
|
|
JSON bool
|
|
}
|
|
|
|
// PruneBlobsResult contains the result of a blob prune operation
|
|
type PruneBlobsResult struct {
|
|
BlobsFound int `json:"blobs_found"`
|
|
BlobsDeleted int `json:"blobs_deleted"`
|
|
BlobsFailed int `json:"blobs_failed,omitempty"`
|
|
BytesFreed int64 `json:"bytes_freed"`
|
|
}
|
|
|
|
// PruneBlobs removes unreferenced blobs from storage
|
|
func (v *Vaultik) PruneBlobs(opts *PruneOptions) error {
|
|
log.Info("Starting prune operation")
|
|
|
|
// Get all remote snapshots and their manifests
|
|
allBlobsReferenced := make(map[string]bool)
|
|
manifestCount := 0
|
|
|
|
// List all snapshots in storage
|
|
log.Info("Listing remote snapshots")
|
|
objectCh := v.Storage.ListStream(v.ctx, "metadata/")
|
|
|
|
var snapshotIDs []string
|
|
for object := range objectCh {
|
|
if object.Err != nil {
|
|
return fmt.Errorf("listing remote snapshots: %w", object.Err)
|
|
}
|
|
|
|
// Extract snapshot ID from paths like metadata/hostname-20240115-143052Z/
|
|
parts := strings.Split(object.Key, "/")
|
|
if len(parts) >= 2 && parts[0] == "metadata" && parts[1] != "" {
|
|
// Check if this is a directory by looking for trailing slash
|
|
if strings.HasSuffix(object.Key, "/") || strings.Contains(object.Key, "/manifest.json.zst") {
|
|
snapshotID := parts[1]
|
|
// Only add unique snapshot IDs
|
|
found := false
|
|
for _, id := range snapshotIDs {
|
|
if id == snapshotID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
snapshotIDs = append(snapshotIDs, snapshotID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Info("Found manifests in remote storage", "count", len(snapshotIDs))
|
|
|
|
// Download and parse each manifest to get referenced blobs
|
|
for _, snapshotID := range snapshotIDs {
|
|
log.Debug("Processing manifest", "snapshot_id", snapshotID)
|
|
|
|
manifest, err := v.downloadManifest(snapshotID)
|
|
if err != nil {
|
|
log.Error("Failed to download manifest", "snapshot_id", snapshotID, "error", err)
|
|
continue
|
|
}
|
|
|
|
// Add all blobs from this manifest to our referenced set
|
|
for _, blob := range manifest.Blobs {
|
|
allBlobsReferenced[blob.Hash] = true
|
|
}
|
|
manifestCount++
|
|
}
|
|
|
|
log.Info("Processed manifests", "count", manifestCount, "unique_blobs_referenced", len(allBlobsReferenced))
|
|
|
|
// List all blobs in storage
|
|
log.Info("Listing all blobs in storage")
|
|
allBlobs := make(map[string]int64) // hash -> size
|
|
blobObjectCh := v.Storage.ListStream(v.ctx, "blobs/")
|
|
|
|
for object := range blobObjectCh {
|
|
if object.Err != nil {
|
|
return fmt.Errorf("listing blobs: %w", object.Err)
|
|
}
|
|
|
|
// Extract hash from path like blobs/ab/cd/abcdef123456...
|
|
parts := strings.Split(object.Key, "/")
|
|
if len(parts) == 4 && parts[0] == "blobs" {
|
|
hash := parts[3]
|
|
allBlobs[hash] = object.Size
|
|
}
|
|
}
|
|
|
|
log.Info("Found blobs in storage", "count", len(allBlobs))
|
|
|
|
// Find unreferenced blobs
|
|
var unreferencedBlobs []string
|
|
var totalSize int64
|
|
for hash, size := range allBlobs {
|
|
if !allBlobsReferenced[hash] {
|
|
unreferencedBlobs = append(unreferencedBlobs, hash)
|
|
totalSize += size
|
|
}
|
|
}
|
|
|
|
result := &PruneBlobsResult{
|
|
BlobsFound: len(unreferencedBlobs),
|
|
}
|
|
|
|
if len(unreferencedBlobs) == 0 {
|
|
log.Info("No unreferenced blobs found")
|
|
if opts.JSON {
|
|
return v.outputPruneBlobsJSON(result)
|
|
}
|
|
v.printlnStdout("No unreferenced blobs to remove.")
|
|
return nil
|
|
}
|
|
|
|
// Show what will be deleted
|
|
log.Info("Found unreferenced blobs", "count", len(unreferencedBlobs), "total_size", humanize.Bytes(uint64(totalSize)))
|
|
if !opts.JSON {
|
|
v.printfStdout("Found %d unreferenced blob(s) totaling %s\n", len(unreferencedBlobs), humanize.Bytes(uint64(totalSize)))
|
|
}
|
|
|
|
// Confirm unless --force is used (skip in JSON mode - require --force)
|
|
if !opts.Force && !opts.JSON {
|
|
v.printfStdout("\nDelete %d unreferenced blob(s)? [y/N] ", len(unreferencedBlobs))
|
|
var confirm string
|
|
if _, err := v.scanStdin(&confirm); err != nil {
|
|
// Treat EOF or error as "no"
|
|
v.printlnStdout("Cancelled")
|
|
return nil
|
|
}
|
|
if strings.ToLower(confirm) != "y" {
|
|
v.printlnStdout("Cancelled")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Delete unreferenced blobs
|
|
log.Info("Deleting unreferenced blobs")
|
|
deletedCount := 0
|
|
deletedSize := int64(0)
|
|
|
|
for i, hash := range unreferencedBlobs {
|
|
blobPath := fmt.Sprintf("blobs/%s/%s/%s", hash[:2], hash[2:4], hash)
|
|
|
|
if err := v.Storage.Delete(v.ctx, blobPath); err != nil {
|
|
log.Error("Failed to delete blob", "hash", hash, "error", err)
|
|
continue
|
|
}
|
|
|
|
deletedCount++
|
|
deletedSize += allBlobs[hash]
|
|
|
|
// Progress update every 100 blobs
|
|
if (i+1)%100 == 0 || i == len(unreferencedBlobs)-1 {
|
|
log.Info("Deletion progress",
|
|
"deleted", i+1,
|
|
"total", len(unreferencedBlobs),
|
|
"percent", fmt.Sprintf("%.1f%%", float64(i+1)/float64(len(unreferencedBlobs))*100),
|
|
)
|
|
}
|
|
}
|
|
|
|
result.BlobsDeleted = deletedCount
|
|
result.BlobsFailed = len(unreferencedBlobs) - deletedCount
|
|
result.BytesFreed = deletedSize
|
|
|
|
log.Info("Prune complete",
|
|
"deleted_count", deletedCount,
|
|
"deleted_size", humanize.Bytes(uint64(deletedSize)),
|
|
"failed", len(unreferencedBlobs)-deletedCount,
|
|
)
|
|
|
|
if opts.JSON {
|
|
return v.outputPruneBlobsJSON(result)
|
|
}
|
|
|
|
v.printfStdout("\nDeleted %d blob(s) totaling %s\n", deletedCount, humanize.Bytes(uint64(deletedSize)))
|
|
if deletedCount < len(unreferencedBlobs) {
|
|
v.printfStdout("Failed to delete %d blob(s)\n", len(unreferencedBlobs)-deletedCount)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// outputPruneBlobsJSON outputs the prune result as JSON
|
|
func (v *Vaultik) outputPruneBlobsJSON(result *PruneBlobsResult) error {
|
|
encoder := json.NewEncoder(v.Stdout)
|
|
encoder.SetIndent("", " ")
|
|
return encoder.Encode(result)
|
|
}
|