snapshot rm: remove metadata only, print prune command for blobs

The previous change had snapshot rm auto-prune unreferenced blobs. The
correct division of labor is: rm removes a snapshot (local DB + remote
metadata), prune cleans up blobs. Reverting the auto-prune means rm
stays a cheap, deterministic operation: it touches one snapshot's worth
of state and emits the exact 'vaultik prune' command the user should
run next to delete blobs no longer referenced by any remote manifest.

This is correct because prune must consult every remote manifest
(including snapshots this host doesn't know about) to determine which
blobs are still referenced, and folding that work into rm would
silently turn rm into an expensive O(remote snapshots) operation that
also assumes the remote is fully reachable.
This commit is contained in:
2026-06-28 06:20:09 +02:00
parent c06d53ad45
commit c17426b556
4 changed files with 54 additions and 107 deletions
+13 -17
View File
@@ -209,7 +209,6 @@ func TestRemoveSnapshot_LocalOnly_PreservesRemote(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "snapshot-001", result.SnapshotID)
assert.False(t, result.RemoteRemoved)
assert.Equal(t, 0, result.BlobsDeleted)
assert.True(t, store.hasKey("blobs/aa/aa/"+blobA))
assert.True(t, store.hasKey(remoteKeyPath("snapshot-001", "manifest.json.zst")))
@@ -217,12 +216,12 @@ func TestRemoveSnapshot_LocalOnly_PreservesRemote(t *testing.T) {
assert.Contains(t, tv.Stdout.String(), "Removed snapshot 'snapshot-001' from local database")
}
// TestRemoveSnapshot_DefaultFullCleanup is the canonical case: no
// flags. The local-DB entry is removed, the snapshot's metadata is
// removed from the destination store, and any blob that was unique to
// this snapshot (i.e. not referenced by any remaining manifest) is
// pruned from the destination store too.
func TestRemoveSnapshot_DefaultFullCleanup(t *testing.T) {
// TestRemoveSnapshot_DefaultRemovesMetadataNotBlobs is the canonical
// case: no flags. The local-DB entry and the snapshot's remote metadata
// are removed; blobs stay on the destination store. The user must then
// run `vaultik prune` to delete blobs no longer referenced by any
// remaining remote manifest, and the output prints that exact command.
func TestRemoveSnapshot_DefaultRemovesMetadataNotBlobs(t *testing.T) {
log.Initialize(log.Config{})
store := newTestStorer()
@@ -243,21 +242,18 @@ func TestRemoveSnapshot_DefaultFullCleanup(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "snapshot-001", result.SnapshotID)
assert.True(t, result.RemoteRemoved)
assert.Equal(t, 1, result.BlobsDeleted, "exactly the unique blob should be deleted")
// Snapshot-001's metadata gone.
assert.False(t, store.hasKey(remoteKeyPath("snapshot-001", "manifest.json.zst")))
// Snapshot-002 untouched.
assert.True(t, store.hasKey(remoteKeyPath("snapshot-002", "manifest.json.zst")))
// Unique blob deleted.
assert.False(t, store.hasKey("blobs/aa/aa/"+blobUnique))
// Shared blob preserved (still referenced by snapshot-002).
// Blobs are intentionally NOT touched — that's what `vaultik prune`
// is for.
assert.True(t, store.hasKey("blobs/aa/aa/"+blobUnique))
assert.True(t, store.hasKey("blobs/bb/bb/"+blobShared))
out := tv.Stdout.String()
assert.Contains(t, out, "Removed snapshot 'snapshot-001' from local database")
assert.Contains(t, out, "Removed snapshot metadata from remote storage")
assert.Contains(t, out, "Removed 1 unreferenced blob")
assert.Contains(t, out, "vaultik prune")
}
func TestRemoveSnapshot_DryRun(t *testing.T) {
@@ -320,16 +316,16 @@ func TestRemoveAllSnapshots_WithForce(t *testing.T) {
require.NoError(t, err)
assert.Len(t, result.SnapshotsRemoved, 2)
assert.True(t, result.RemoteRemoved)
assert.Equal(t, 1, result.BlobsDeleted)
assert.False(t, store.hasKey("blobs/aa/aa/"+blobA))
// Blobs intentionally preserved — that's prune's job.
assert.True(t, store.hasKey("blobs/aa/aa/"+blobA))
assert.False(t, store.hasKey(remoteKeyPath("snapshot-001", "manifest.json.zst")))
assert.False(t, store.hasKey(remoteKeyPath("snapshot-002", "manifest.json.zst")))
out := tv.Stdout.String()
assert.Contains(t, out, "Removed 2 snapshot(s)")
assert.Contains(t, out, "Removed snapshot metadata from remote storage")
assert.Contains(t, out, "Removed 1 unreferenced blob")
assert.Contains(t, out, "vaultik prune")
}
func TestRemoveAllSnapshots_DryRun(t *testing.T) {