Merge branch 'fix/snapshot-rm-no-auto-prune'
All checks were successful
check / check (push) Successful in 1m54s
All checks were successful
check / check (push) Successful in 1m54s
This commit is contained in:
17
README.md
17
README.md
@@ -199,13 +199,16 @@ latest globally).
|
|||||||
* `--force`: Skip confirmation prompt
|
* `--force`: Skip confirmation prompt
|
||||||
|
|
||||||
**`snapshot remove`**: Remove a snapshot. By default this removes the
|
**`snapshot remove`**: Remove a snapshot. By default this removes the
|
||||||
snapshot from the local index, strips the snapshot's metadata from the
|
snapshot from the local index and strips the snapshot's metadata from
|
||||||
backup destination store, and prunes any blobs that are no longer
|
the backup destination store. Blobs are NOT touched — deleting blobs
|
||||||
referenced by any remaining remote manifest. Local row cleanup (files,
|
requires reading every remaining remote manifest (the destination store
|
||||||
chunks, blobs the snapshot was the last referrer for) runs automatically;
|
may hold snapshots this host doesn't know about), which is what
|
||||||
no separate prune step is needed. If the destination store is unreachable,
|
`vaultik prune` does. On success the command prints the exact `vaultik
|
||||||
the local-DB removal still completes and a warning is emitted; rerun
|
prune` invocation to run as a follow-up. Local row cleanup (files,
|
||||||
`vaultik prune` once the store is reachable to finish remote cleanup.
|
chunks, blobs the snapshot was the last referrer for) runs
|
||||||
|
automatically. If the destination store is unreachable, the local-DB
|
||||||
|
removal still completes and a warning is emitted; rerun `vaultik prune`
|
||||||
|
once the store is reachable to finish remote cleanup.
|
||||||
* `--local-only`: Skip remote cleanup; only touch the local index
|
* `--local-only`: Skip remote cleanup; only touch the local index
|
||||||
* `--all`: Remove all snapshots (requires `--force`)
|
* `--all`: Remove all snapshots (requires `--force`)
|
||||||
* `--dry-run`: Show what would be deleted without deleting
|
* `--dry-run`: Show what would be deleted without deleting
|
||||||
|
|||||||
@@ -324,12 +324,15 @@ func newSnapshotRemoveCommand() *cobra.Command {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "remove [snapshot-id]",
|
Use: "remove [snapshot-id]",
|
||||||
Aliases: []string{"rm"},
|
Aliases: []string{"rm"},
|
||||||
Short: "Remove a snapshot from local index and remote storage",
|
Short: "Remove a snapshot from local index and remote metadata",
|
||||||
Long: `Removes a snapshot.
|
Long: `Removes a snapshot.
|
||||||
|
|
||||||
By default, this removes the snapshot from the local index database, strips
|
By default, this removes the snapshot from the local index database and
|
||||||
the snapshot's metadata from the backup destination store, and prunes any
|
strips the snapshot's metadata from the backup destination store. Blobs
|
||||||
blobs that are no longer referenced by any remaining remote snapshot.
|
are NOT touched: deleting them requires reading every remaining remote
|
||||||
|
manifest (the destination store may hold snapshots this host doesn't
|
||||||
|
know about), which is what 'vaultik prune' does. On success the command
|
||||||
|
prints the exact 'vaultik prune' invocation to run as a follow-up.
|
||||||
|
|
||||||
Use --local-only to skip the remote half (e.g. when you want to forget a
|
Use --local-only to skip the remote half (e.g. when you want to forget a
|
||||||
snapshot locally without touching the destination store).
|
snapshot locally without touching the destination store).
|
||||||
|
|||||||
@@ -209,7 +209,6 @@ func TestRemoveSnapshot_LocalOnly_PreservesRemote(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "snapshot-001", result.SnapshotID)
|
assert.Equal(t, "snapshot-001", result.SnapshotID)
|
||||||
assert.False(t, result.RemoteRemoved)
|
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("blobs/aa/aa/"+blobA))
|
||||||
assert.True(t, store.hasKey(remoteKeyPath("snapshot-001", "manifest.json.zst")))
|
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")
|
assert.Contains(t, tv.Stdout.String(), "Removed snapshot 'snapshot-001' from local database")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRemoveSnapshot_DefaultFullCleanup is the canonical case: no
|
// TestRemoveSnapshot_DefaultRemovesMetadataNotBlobs is the canonical
|
||||||
// flags. The local-DB entry is removed, the snapshot's metadata is
|
// case: no flags. The local-DB entry and the snapshot's remote metadata
|
||||||
// removed from the destination store, and any blob that was unique to
|
// are removed; blobs stay on the destination store. The user must then
|
||||||
// this snapshot (i.e. not referenced by any remaining manifest) is
|
// run `vaultik prune` to delete blobs no longer referenced by any
|
||||||
// pruned from the destination store too.
|
// remaining remote manifest, and the output prints that exact command.
|
||||||
func TestRemoveSnapshot_DefaultFullCleanup(t *testing.T) {
|
func TestRemoveSnapshot_DefaultRemovesMetadataNotBlobs(t *testing.T) {
|
||||||
log.Initialize(log.Config{})
|
log.Initialize(log.Config{})
|
||||||
|
|
||||||
store := newTestStorer()
|
store := newTestStorer()
|
||||||
@@ -243,21 +242,18 @@ func TestRemoveSnapshot_DefaultFullCleanup(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "snapshot-001", result.SnapshotID)
|
assert.Equal(t, "snapshot-001", result.SnapshotID)
|
||||||
assert.True(t, result.RemoteRemoved)
|
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")))
|
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")))
|
assert.True(t, store.hasKey(remoteKeyPath("snapshot-002", "manifest.json.zst")))
|
||||||
// Unique blob deleted.
|
// Blobs are intentionally NOT touched — that's what `vaultik prune`
|
||||||
assert.False(t, store.hasKey("blobs/aa/aa/"+blobUnique))
|
// is for.
|
||||||
// Shared blob preserved (still referenced by snapshot-002).
|
assert.True(t, store.hasKey("blobs/aa/aa/"+blobUnique))
|
||||||
assert.True(t, store.hasKey("blobs/bb/bb/"+blobShared))
|
assert.True(t, store.hasKey("blobs/bb/bb/"+blobShared))
|
||||||
|
|
||||||
out := tv.Stdout.String()
|
out := tv.Stdout.String()
|
||||||
assert.Contains(t, out, "Removed snapshot 'snapshot-001' from local database")
|
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 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) {
|
func TestRemoveSnapshot_DryRun(t *testing.T) {
|
||||||
@@ -320,16 +316,16 @@ func TestRemoveAllSnapshots_WithForce(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, result.SnapshotsRemoved, 2)
|
assert.Len(t, result.SnapshotsRemoved, 2)
|
||||||
assert.True(t, result.RemoteRemoved)
|
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-001", "manifest.json.zst")))
|
||||||
assert.False(t, store.hasKey(remoteKeyPath("snapshot-002", "manifest.json.zst")))
|
assert.False(t, store.hasKey(remoteKeyPath("snapshot-002", "manifest.json.zst")))
|
||||||
|
|
||||||
out := tv.Stdout.String()
|
out := tv.Stdout.String()
|
||||||
assert.Contains(t, out, "Removed 2 snapshot(s)")
|
assert.Contains(t, out, "Removed 2 snapshot(s)")
|
||||||
assert.Contains(t, out, "Removed snapshot metadata from remote storage")
|
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) {
|
func TestRemoveAllSnapshots_DryRun(t *testing.T) {
|
||||||
|
|||||||
@@ -985,17 +985,22 @@ type RemoveResult struct {
|
|||||||
SnapshotID string `json:"snapshot_id,omitempty"`
|
SnapshotID string `json:"snapshot_id,omitempty"`
|
||||||
SnapshotsRemoved []string `json:"snapshots_removed,omitempty"`
|
SnapshotsRemoved []string `json:"snapshots_removed,omitempty"`
|
||||||
RemoteRemoved bool `json:"remote_removed,omitempty"`
|
RemoteRemoved bool `json:"remote_removed,omitempty"`
|
||||||
BlobsDeleted int `json:"blobs_deleted,omitempty"`
|
|
||||||
BytesFreed int64 `json:"bytes_freed,omitempty"`
|
|
||||||
DryRun bool `json:"dry_run,omitempty"`
|
DryRun bool `json:"dry_run,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pruneCommandHint is the exact command suggested after a remove
|
||||||
|
// completes so the user knows how to clean up the blobs that the
|
||||||
|
// just-removed snapshot left behind on the destination store.
|
||||||
|
const pruneCommandHint = "vaultik prune"
|
||||||
|
|
||||||
// RemoveSnapshot removes a snapshot from the local index database and,
|
// RemoveSnapshot removes a snapshot from the local index database and,
|
||||||
// unless LocalOnly is set, also strips the snapshot's metadata from the
|
// unless LocalOnly is set, also strips the snapshot's metadata from the
|
||||||
// destination store and prunes any blobs that are no longer referenced
|
// destination store. Blobs are NOT touched: removing a snapshot's
|
||||||
// by any remaining remote snapshot. When the remote is unreachable the
|
// blobs requires reading every remaining remote manifest (the remote
|
||||||
// command still completes the local-DB removal and warns; callers can
|
// may hold snapshots this host doesn't know about), which is what
|
||||||
// retry remote cleanup later with `vaultik prune`.
|
// `vaultik prune` is for — the command prints the prune invocation to
|
||||||
|
// run as a follow-up. When the remote is unreachable the command still
|
||||||
|
// completes the local-DB removal and warns.
|
||||||
func (v *Vaultik) RemoveSnapshot(snapshotID string, opts *RemoveOptions) (*RemoveResult, error) {
|
func (v *Vaultik) RemoveSnapshot(snapshotID string, opts *RemoveOptions) (*RemoveResult, error) {
|
||||||
result := &RemoveResult{
|
result := &RemoveResult{
|
||||||
SnapshotID: snapshotID,
|
SnapshotID: snapshotID,
|
||||||
@@ -1006,7 +1011,7 @@ func (v *Vaultik) RemoveSnapshot(snapshotID string, opts *RemoveOptions) (*Remov
|
|||||||
if !opts.JSON {
|
if !opts.JSON {
|
||||||
v.printfStdout("Would remove snapshot: %s\n", snapshotID)
|
v.printfStdout("Would remove snapshot: %s\n", snapshotID)
|
||||||
if !opts.LocalOnly {
|
if !opts.LocalOnly {
|
||||||
v.printlnStdout("Would also remove metadata and any unique blobs from remote storage")
|
v.printlnStdout("Would also remove snapshot metadata from remote storage (blobs untouched)")
|
||||||
}
|
}
|
||||||
v.printlnStdout("[Dry run - no changes made]")
|
v.printlnStdout("[Dry run - no changes made]")
|
||||||
}
|
}
|
||||||
@@ -1020,7 +1025,7 @@ func (v *Vaultik) RemoveSnapshot(snapshotID string, opts *RemoveOptions) (*Remov
|
|||||||
if opts.LocalOnly {
|
if opts.LocalOnly {
|
||||||
v.printfStdout("Remove snapshot '%s' from local database (remote untouched)? [y/N] ", snapshotID)
|
v.printfStdout("Remove snapshot '%s' from local database (remote untouched)? [y/N] ", snapshotID)
|
||||||
} else {
|
} else {
|
||||||
v.printfStdout("Remove snapshot '%s' from local database AND remote storage (including any blobs unique to this snapshot)? [y/N] ", snapshotID)
|
v.printfStdout("Remove snapshot '%s' from local database AND its metadata from remote storage? [y/N] ", snapshotID)
|
||||||
}
|
}
|
||||||
var confirm string
|
var confirm string
|
||||||
if _, err := v.scanStdin(&confirm); err != nil {
|
if _, err := v.scanStdin(&confirm); err != nil {
|
||||||
@@ -1043,25 +1048,16 @@ func (v *Vaultik) RemoveSnapshot(snapshotID string, opts *RemoveOptions) (*Remov
|
|||||||
log.Info("Removing snapshot metadata from remote storage", "snapshot_id", snapshotID)
|
log.Info("Removing snapshot metadata from remote storage", "snapshot_id", snapshotID)
|
||||||
remoteKey := snapshot.RemoteSnapshotKey(snapshotID)
|
remoteKey := snapshot.RemoteSnapshotKey(snapshotID)
|
||||||
if err := v.deleteRemoteSnapshotByKey(remoteKey); err != nil {
|
if err := v.deleteRemoteSnapshotByKey(remoteKey); err != nil {
|
||||||
// Per design: warn-and-proceed; the local-DB removal has
|
// Warn-and-proceed: the local-DB removal has already
|
||||||
// already happened, so the user can retry remote cleanup
|
// happened, so let the user know the remote half didn't
|
||||||
// with `vaultik prune` later.
|
// finish and they can retry with `vaultik prune` once the
|
||||||
|
// destination store is reachable.
|
||||||
log.Warn("Could not remove snapshot metadata from remote storage", "error", err)
|
log.Warn("Could not remove snapshot metadata from remote storage", "error", err)
|
||||||
if v.UI != nil {
|
if v.UI != nil {
|
||||||
v.UI.Warning("Could not remove snapshot metadata from remote: %v. Run 'vaultik prune' once the remote is reachable to finish cleanup.", err)
|
v.UI.Warning("Could not remove snapshot metadata from remote: %v. Run '%s' once the remote is reachable to finish cleanup.", err, pruneCommandHint)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result.RemoteRemoved = true
|
result.RemoteRemoved = true
|
||||||
blobsDeleted, bytesFreed, pruneErr := v.pruneUnreferencedBlobsAfterRemoval()
|
|
||||||
if pruneErr != nil {
|
|
||||||
log.Warn("Failed to prune unreferenced blobs after snapshot removal", "error", pruneErr)
|
|
||||||
if v.UI != nil {
|
|
||||||
v.UI.Warning("Snapshot metadata removed, but blob cleanup failed: %v. Run 'vaultik prune' to retry.", pruneErr)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result.BlobsDeleted = blobsDeleted
|
|
||||||
result.BytesFreed = bytesFreed
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1078,47 +1074,12 @@ func (v *Vaultik) RemoveSnapshot(snapshotID string, opts *RemoveOptions) (*Remov
|
|||||||
v.printfStdout("Removed snapshot '%s' from local database\n", snapshotID)
|
v.printfStdout("Removed snapshot '%s' from local database\n", snapshotID)
|
||||||
if !opts.LocalOnly && result.RemoteRemoved {
|
if !opts.LocalOnly && result.RemoteRemoved {
|
||||||
v.printlnStdout("Removed snapshot metadata from remote storage")
|
v.printlnStdout("Removed snapshot metadata from remote storage")
|
||||||
if result.BlobsDeleted > 0 {
|
v.printfStdout("\nNote: The removed snapshot's blobs remain on the remote. Run '%s' to delete any blobs no longer referenced by any remaining remote snapshot.\n", pruneCommandHint)
|
||||||
v.printfStdout("Removed %d unreferenced blob(s) (%s freed)\n",
|
|
||||||
result.BlobsDeleted, humanize.Bytes(uint64(result.BytesFreed)))
|
|
||||||
} else {
|
|
||||||
v.printlnStdout("No blobs unique to this snapshot were found.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pruneUnreferencedBlobsAfterRemoval deletes blobs no longer referenced
|
|
||||||
// by any remaining remote manifest. Used by RemoveSnapshot /
|
|
||||||
// RemoveAllSnapshots after metadata has been stripped from the
|
|
||||||
// destination store; with the just-removed snapshot's manifest gone,
|
|
||||||
// any blobs that were only referenced by it become unreferenced and
|
|
||||||
// are swept here.
|
|
||||||
func (v *Vaultik) pruneUnreferencedBlobsAfterRemoval() (int, int64, error) {
|
|
||||||
referenced, err := v.collectReferencedBlobs()
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, fmt.Errorf("collecting referenced blobs: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
allBlobs, err := v.listAllRemoteBlobs()
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, fmt.Errorf("listing remote blobs: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
unreferenced, totalSize := v.findUnreferencedBlobs(allBlobs, referenced)
|
|
||||||
if len(unreferenced) == 0 {
|
|
||||||
return 0, 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := &PruneBlobsResult{BlobsFound: len(unreferenced)}
|
|
||||||
log.Info("Pruning unreferenced blobs after snapshot removal",
|
|
||||||
"count", len(unreferenced),
|
|
||||||
"size", humanize.Bytes(uint64(totalSize)))
|
|
||||||
v.deleteUnreferencedBlobs(unreferenced, allBlobs, result)
|
|
||||||
return result.BlobsDeleted, result.BytesFreed, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveAllSnapshots removes every snapshot known to the local
|
// RemoveAllSnapshots removes every snapshot known to the local
|
||||||
// database from the local index, and (with --remote) every snapshot
|
// database from the local index, and (with --remote) every snapshot
|
||||||
// metadata directory in remote storage. Both sides are processed so a
|
// metadata directory in remote storage. Both sides are processed so a
|
||||||
@@ -1241,9 +1202,8 @@ func (v *Vaultik) handleRemoveAllDryRun(localSnaps, orphanRemoteKeys []string, o
|
|||||||
v.printfStdout(" %s\n", key)
|
v.printfStdout(" %s\n", key)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
v.printlnStdout("Would also remove from remote storage")
|
v.printlnStdout("Would also remove snapshot metadata from remote storage (blobs untouched)")
|
||||||
}
|
}
|
||||||
v.printlnStdout("Would then prune all unreferenced blobs from remote storage")
|
|
||||||
}
|
}
|
||||||
v.printlnStdout("[Dry run - no changes made]")
|
v.printlnStdout("[Dry run - no changes made]")
|
||||||
}
|
}
|
||||||
@@ -1255,8 +1215,8 @@ func (v *Vaultik) handleRemoveAllDryRun(localSnaps, orphanRemoteKeys []string, o
|
|||||||
|
|
||||||
// executeRemoveAll deletes every local snapshot and, unless LocalOnly
|
// executeRemoveAll deletes every local snapshot and, unless LocalOnly
|
||||||
// is set, every corresponding remote metadata directory plus any
|
// is set, every corresponding remote metadata directory plus any
|
||||||
// orphan remote keys, then prunes the resulting set of unreferenced
|
// orphan remote keys. Blobs are not touched; the printed prune-command
|
||||||
// blobs from the destination store.
|
// hint is the next step.
|
||||||
func (v *Vaultik) executeRemoveAll(localSnaps, orphanRemoteKeys []string, opts *RemoveOptions) (*RemoveResult, error) {
|
func (v *Vaultik) executeRemoveAll(localSnaps, orphanRemoteKeys []string, opts *RemoveOptions) (*RemoveResult, error) {
|
||||||
if !opts.Force {
|
if !opts.Force {
|
||||||
return nil, fmt.Errorf("--all requires --force")
|
return nil, fmt.Errorf("--all requires --force")
|
||||||
@@ -1297,18 +1257,8 @@ func (v *Vaultik) executeRemoveAll(localSnaps, orphanRemoteKeys []string, opts *
|
|||||||
|
|
||||||
if remoteErrors == 0 {
|
if remoteErrors == 0 {
|
||||||
result.RemoteRemoved = true
|
result.RemoteRemoved = true
|
||||||
blobsDeleted, bytesFreed, pruneErr := v.pruneUnreferencedBlobsAfterRemoval()
|
|
||||||
if pruneErr != nil {
|
|
||||||
log.Warn("Failed to prune unreferenced blobs after bulk removal", "error", pruneErr)
|
|
||||||
if v.UI != nil {
|
|
||||||
v.UI.Warning("Bulk metadata removal succeeded, but blob cleanup failed: %v. Run 'vaultik prune' to retry.", pruneErr)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result.BlobsDeleted = blobsDeleted
|
|
||||||
result.BytesFreed = bytesFreed
|
|
||||||
}
|
|
||||||
} else if v.UI != nil {
|
} else if v.UI != nil {
|
||||||
v.UI.Warning("Some remote metadata deletions failed; skipping automatic blob prune. Run 'vaultik prune' once the remote is healthy.")
|
v.UI.Warning("Some remote metadata deletions failed. Run '%s' once the remote is healthy to clean up unreferenced blobs.", pruneCommandHint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1325,12 +1275,7 @@ func (v *Vaultik) executeRemoveAll(localSnaps, orphanRemoteKeys []string, opts *
|
|||||||
v.printfStdout("Removed %d snapshot(s)\n", len(result.SnapshotsRemoved))
|
v.printfStdout("Removed %d snapshot(s)\n", len(result.SnapshotsRemoved))
|
||||||
if !opts.LocalOnly && result.RemoteRemoved {
|
if !opts.LocalOnly && result.RemoteRemoved {
|
||||||
v.printlnStdout("Removed snapshot metadata from remote storage")
|
v.printlnStdout("Removed snapshot metadata from remote storage")
|
||||||
if result.BlobsDeleted > 0 {
|
v.printfStdout("\nNote: Removed snapshots' blobs remain on the remote. Run '%s' to delete any blobs no longer referenced by any remaining remote snapshot.\n", pruneCommandHint)
|
||||||
v.printfStdout("Removed %d unreferenced blob(s) (%s freed)\n",
|
|
||||||
result.BlobsDeleted, humanize.Bytes(uint64(result.BytesFreed)))
|
|
||||||
} else {
|
|
||||||
v.printlnStdout("No unreferenced blobs were found.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user