From cafb3d45b8bca291ac14b5252a822b803342d1e4 Mon Sep 17 00:00:00 2001 From: clawbot Date: Sun, 8 Feb 2026 08:34:17 -0800 Subject: [PATCH 1/3] fix: track and report file restore failures Restore previously logged errors for individual files but returned success even if files failed. Now tracks failed files in RestoreResult, reports them in the summary output, and returns an error if any files failed to restore. Fixes #21 --- internal/vaultik/restore.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/internal/vaultik/restore.go b/internal/vaultik/restore.go index aa00a27..d56fcca 100644 --- a/internal/vaultik/restore.go +++ b/internal/vaultik/restore.go @@ -118,6 +118,8 @@ func (v *Vaultik) Restore(opts *RestoreOptions) error { if err := v.restoreFile(v.ctx, repos, file, opts.TargetDir, identity, chunkToBlobMap, blobCache, result); err != nil { log.Error("Failed to restore file", "path", file.Path, "error", err) + result.FilesFailed++ + result.FailedFiles = append(result.FailedFiles, file.Path.String()) // Continue with other files continue } @@ -147,6 +149,13 @@ func (v *Vaultik) Restore(opts *RestoreOptions) error { result.Duration.Round(time.Second), ) + if result.FilesFailed > 0 { + _, _ = fmt.Fprintf(v.Stdout, "\nWARNING: %d file(s) failed to restore:\n", result.FilesFailed) + for _, path := range result.FailedFiles { + _, _ = fmt.Fprintf(v.Stdout, " - %s\n", path) + } + } + // Run verification if requested if opts.Verify { if err := v.verifyRestoredFiles(v.ctx, repos, files, opts.TargetDir, result); err != nil { @@ -167,6 +176,10 @@ func (v *Vaultik) Restore(opts *RestoreOptions) error { ) } + if result.FilesFailed > 0 { + return fmt.Errorf("%d file(s) failed to restore", result.FilesFailed) + } + return nil } From ddc23f8057ff3c874c6e7faaed3958c6c539453d Mon Sep 17 00:00:00 2001 From: clawbot Date: Sun, 8 Feb 2026 12:01:51 -0800 Subject: [PATCH 2/3] fix: return errors from deleteSnapshotFromLocalDB instead of swallowing them Previously, deleteSnapshotFromLocalDB logged errors but always returned nil, causing callers to believe deletion succeeded even when it failed. This could lead to data inconsistency where remote metadata is deleted while local records persist. Now returns the first error encountered, allowing callers to handle failures appropriately. --- internal/vaultik/snapshot.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/vaultik/snapshot.go b/internal/vaultik/snapshot.go index a6e498b..1391222 100644 --- a/internal/vaultik/snapshot.go +++ b/internal/vaultik/snapshot.go @@ -1004,16 +1004,16 @@ func (v *Vaultik) deleteSnapshotFromLocalDB(snapshotID string) error { // Delete related records first to avoid foreign key constraints if err := v.Repositories.Snapshots.DeleteSnapshotFiles(v.ctx, snapshotID); err != nil { - log.Error("Failed to delete snapshot files", "snapshot_id", snapshotID, "error", err) + return fmt.Errorf("deleting snapshot files for %s: %w", snapshotID, err) } if err := v.Repositories.Snapshots.DeleteSnapshotBlobs(v.ctx, snapshotID); err != nil { - log.Error("Failed to delete snapshot blobs", "snapshot_id", snapshotID, "error", err) + return fmt.Errorf("deleting snapshot blobs for %s: %w", snapshotID, err) } if err := v.Repositories.Snapshots.DeleteSnapshotUploads(v.ctx, snapshotID); err != nil { - log.Error("Failed to delete snapshot uploads", "snapshot_id", snapshotID, "error", err) + return fmt.Errorf("deleting snapshot uploads for %s: %w", snapshotID, err) } if err := v.Repositories.Snapshots.Delete(v.ctx, snapshotID); err != nil { - log.Error("Failed to delete snapshot record", "snapshot_id", snapshotID, "error", err) + return fmt.Errorf("deleting snapshot record %s: %w", snapshotID, err) } return nil From 76e047bbb287acfdf27fa300fbc684ca5a607503 Mon Sep 17 00:00:00 2001 From: clawbot Date: Sun, 15 Feb 2026 21:34:46 -0800 Subject: [PATCH 3/3] feat: implement --prune flag on snapshot create (closes #4) The --prune flag on 'snapshot create' was accepted but silently did nothing (TODO stub). This connects it to actually: 1. Purge old snapshots (keeping only the latest) via PurgeSnapshots 2. Remove unreferenced blobs from storage via PruneBlobs The pruning runs after all snapshots complete successfully, not per-snapshot. Both operations use --force mode (no interactive confirmation) since --prune is an explicit opt-in flag. Moved the prune logic from createNamedSnapshot (per-snapshot) to CreateSnapshot (after all snapshots), which is the correct location. --- internal/vaultik/snapshot.go | 23 ++++++++++++++++++----- internal/vaultik/snapshot_prune_test.go | 23 +++++++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 internal/vaultik/snapshot_prune_test.go diff --git a/internal/vaultik/snapshot.go b/internal/vaultik/snapshot.go index 2960389..94df29b 100644 --- a/internal/vaultik/snapshot.go +++ b/internal/vaultik/snapshot.go @@ -90,6 +90,24 @@ func (v *Vaultik) CreateSnapshot(opts *SnapshotCreateOptions) error { v.printfStdout("\nAll %d snapshots completed in %s\n", len(snapshotNames), time.Since(overallStartTime).Round(time.Second)) } + // Prune old snapshots and unreferenced blobs if --prune was specified + if opts.Prune { + log.Info("Pruning enabled - deleting old snapshots and unreferenced blobs") + v.printlnStdout("\nPruning old snapshots (keeping latest)...") + + if err := v.PurgeSnapshots(true, "", true); err != nil { + return fmt.Errorf("prune: purging old snapshots: %w", err) + } + + v.printlnStdout("Pruning unreferenced blobs...") + + if err := v.PruneBlobs(&PruneOptions{Force: true}); err != nil { + return fmt.Errorf("prune: removing unreferenced blobs: %w", err) + } + + log.Info("Pruning complete") + } + return nil } @@ -306,11 +324,6 @@ func (v *Vaultik) createNamedSnapshot(opts *SnapshotCreateOptions, hostname, sna } v.printfStdout("Duration: %s\n", formatDuration(snapshotDuration)) - if opts.Prune { - log.Info("Pruning enabled - will delete old snapshots after snapshot") - // TODO: Implement pruning - } - return nil } diff --git a/internal/vaultik/snapshot_prune_test.go b/internal/vaultik/snapshot_prune_test.go new file mode 100644 index 0000000..dbff412 --- /dev/null +++ b/internal/vaultik/snapshot_prune_test.go @@ -0,0 +1,23 @@ +package vaultik + +import ( + "testing" +) + +// TestSnapshotCreateOptions_PruneFlag verifies the Prune field exists on +// SnapshotCreateOptions and can be set. +func TestSnapshotCreateOptions_PruneFlag(t *testing.T) { + opts := &SnapshotCreateOptions{ + Prune: true, + } + if !opts.Prune { + t.Error("Expected Prune to be true") + } + + opts2 := &SnapshotCreateOptions{ + Prune: false, + } + if opts2.Prune { + t.Error("Expected Prune to be false") + } +}