Bind the local index to its backup destination

The local index tracks which chunks and blobs already exist at the
backup destination. Nothing was recording *which* destination, so
changing storage_url and running a backup left the scanner treating
every already-seen chunk as still-present at the new (empty) location.
Uploads were skipped silently and the resulting snapshots pointed at
blobs that don't exist at the new destination.

Fix: record storage_url in a new local_meta key-value table on first
mutating command, and refuse to proceed when the configured URL later
differs from the stored one. The error explains the two recovery
paths (revert the config, or run 'vaultik database purge' to discard
the index and rebuild from a fresh full backup).

Wired into snapshot create / prune / snapshot remove / snapshot purge
/ snapshot cleanup. Read-only inspection commands (snapshot list,
remote info, store info) are exempt.
This commit is contained in:
2026-07-02 16:35:41 +02:00
parent fda6d7a7eb
commit d330f9f031
8 changed files with 304 additions and 0 deletions
+17
View File
@@ -36,6 +36,10 @@ func (v *Vaultik) CreateSnapshot(opts *SnapshotCreateOptions) error {
"index_path", v.Config.IndexPath,
)
if err := v.EnsureStorageBinding(); err != nil {
return err
}
// Clean up incomplete snapshots FIRST, before any scanning
// This is critical for data safety - see CleanupIncompleteSnapshots for details
hostname := v.Config.Hostname
@@ -581,6 +585,9 @@ type SnapshotPurgeOptions struct {
// snapshot name, not the latest globally. This prevents `home` and `system`
// snapshots from cannibalizing each other.
func (v *Vaultik) PurgeSnapshotsWithOptions(opts *SnapshotPurgeOptions) error {
if err := v.EnsureStorageBinding(); err != nil {
return err
}
// Sync with remote first
if err := v.syncWithRemote(); err != nil {
return fmt.Errorf("syncing with remote: %w", err)
@@ -861,6 +868,9 @@ func (v *Vaultik) outputVerifyJSON(result *VerifyResult) error {
// human ID is hashed via RemoteSnapshotKey and compared against the
// remote listing.
func (v *Vaultik) CleanupLocalSnapshots() error {
if err := v.EnsureStorageBinding(); err != nil {
return err
}
remoteKeys, err := v.listAllRemoteSnapshotKeys()
if err != nil {
return err
@@ -1006,6 +1016,10 @@ func (v *Vaultik) RemoveSnapshot(snapshotID string, opts *RemoveOptions) (*Remov
SnapshotID: snapshotID,
}
if err := v.EnsureStorageBinding(); err != nil {
return result, err
}
if opts.DryRun {
result.DryRun = true
if !opts.JSON {
@@ -1086,6 +1100,9 @@ func (v *Vaultik) RemoveSnapshot(snapshotID string, opts *RemoveOptions) (*Remov
// "remove --all" leaves nothing behind, even when the local DB and
// remote storage have diverged.
func (v *Vaultik) RemoveAllSnapshots(opts *RemoveOptions) (*RemoveResult, error) {
if err := v.EnsureStorageBinding(); err != nil {
return nil, err
}
localSnaps, err := v.localSnapshotIDs()
if err != nil {
return nil, fmt.Errorf("listing local snapshots: %w", err)