1 Commits
Author SHA1 Message Date
sneak 39ca0e3111 Make snapshot list report a changed destination instead of advising prune (closes #317)
check / check (push) Canceled after 0s
When the local index is bound to a different destination than the
configured one, snapshot list now prints the warning prune, snapshot
purge and snapshot remove refuse with, including its two ways out
(revert the configuration, or run vaultik database delete). It no
longer reports the local records as missing or advises vaultik prune,
which would refuse.

Reading the binding moves out of EnsureStorageBinding into
storageBinding, which records nothing, so the read-only list can call
it without binding a fresh index.

Judgement call: --json output is unchanged; it gives no advice.

Model: opus-5-5
2026-10-13 22:45:10 +00:00
3 changed files with 12 additions and 57 deletions
+3 -5
View File
@@ -336,11 +336,9 @@ local index alone, and still exits zero.
(whether the snapshot is in the local index), `remote_key` (the full
64-character storage key), and `remote_present` (whether it was seen
on the destination store, or `null` if the destination could not be
listed). Warnings about an unlistable destination, a local index
bound to a different destination (with both destinations and the
same two ways out), unreadable manifests, and a truncated listing all
go to stderr through the logger, so stdout stays a single parseable
document.
listed). Warnings about an unlistable destination, unreadable
manifests, and a truncated listing all go to stderr through the
logger, so stdout stays a single parseable document.
**`snapshot verify`**: Verify snapshot integrity.
* Default (shallow): checks that every blob the manifest lists is present in
+9 -19
View File
@@ -105,26 +105,7 @@ func (v *Vaultik) ListSnapshots(jsonOutput bool) error {
return snapshots[i].Timestamp.After(snapshots[j].Timestamp)
})
stored, configured, err := v.storageBinding()
if err != nil {
return err
}
// A local index bound to another destination describes that one, so
// its records are expected to be missing here, and prune refuses to
// run. The binding is reported instead of advising prune.
boundElsewhere := stored != "" && stored != configured
if jsonOutput {
// stdout holds only the document, so this goes to the logger.
if boundElsewhere {
log.Warn("Local index is bound to a different backup "+
"destination; to proceed, revert storage_url (or the s3.* "+
"endpoint, bucket and prefix) to the bound destination, "+
"or run 'vaultik database delete'",
"bound_to", stored, "configured", configured)
}
if remoteErr == nil {
v.reportJSONListingLimits(listing)
}
@@ -146,6 +127,15 @@ func (v *Vaultik) ListSnapshots(jsonOutput bool) error {
}
}
stored, configured, err := v.storageBinding()
if err != nil {
return err
}
// A local index bound to another destination describes that one, so
// its records are expected to be missing here, and prune refuses to
// run. The binding is reported instead of advising prune.
boundElsewhere := stored != "" && stored != configured
if boundElsewhere {
v.UI.Warningf("%v\n%s", errStorageBindingMismatch,
buildBindingMismatchMessage(stored, configured))
-33
View File
@@ -456,39 +456,6 @@ func TestListSnapshots_BoundElsewhereDoesNotAdvisePrune(t *testing.T) {
assert.NotContains(t, out, "vaultik prune")
}
// TestListSnapshots_JSONReportsBoundElsewhere is the --json side of the
// test above. Every local row comes out with remote_present false, which
// a script reads as stale local records unless it is told, on stderr,
// that the local index is bound to a different destination.
//
//nolint:paralleltest // captureProcessStderr replaces os.Stderr
func TestListSnapshots_JSONReportsBoundElsewhere(t *testing.T) {
const (
oldURL = "file:///mnt/backups/old"
newURL = "file:///mnt/backups/new"
)
env := newListEnv(t)
env.v.Config.StorageURL = newURL
require.NoError(t, env.v.Repositories.LocalMeta.Set(context.Background(),
database.LocalMetaKeyStorageURL, oldURL))
env.addLocal(t, listLocalID, time.Date(2026, 3, 1, 10, 0, 0, 0, time.UTC))
stderr := captureProcessStderr(t, func() {
require.NoError(t, env.v.ListSnapshots(true))
})
rows := decodeListJSON(t, env.stdout.String())
require.Len(t, rows, 1)
assert.Equal(t, listLocalID, rows[0].ID)
assert.Contains(t, stderr, "bound to a different backup destination")
assert.Contains(t, stderr, oldURL)
assert.Contains(t, stderr, newURL)
assert.Contains(t, stderr, "revert storage_url")
assert.Contains(t, stderr, "vaultik database delete")
}
// TestListSnapshots_UnreachableRemoteDegrades covers the promise in the
// doc comment: an unreachable destination is a warning plus local-only
// output, never a failure.