Reconcile purge against remote by hashed key, not human ID (closes #160)
check / check (pull_request) Successful in 1m20s
check / check (push) Successful in 2m51s

syncWithRemote compared human snapshot IDs against the hashed metadata/<key>/ directory names, which never match, so it deleted every local snapshot record; the purge that followed then found nothing to remove remotely. Reconcile via listAllRemoteSnapshotKeys and RemoteSnapshotKey(id), matching CleanupLocalSnapshots, so a row still backed by remote metadata is kept.

The purge tests only passed because their stubs used the human-ID layout production never writes; they now write metadata under the hashed remote key. New tests prove remotely-backed local rows survive the reconcile and that a purge removes the local row and remote metadata together.

Model: opus-4-8
This commit was merged in pull request #183.
This commit is contained in:
2026-09-22 12:11:49 +02:00
parent d9f0220f94
commit 96ebcd40d7
3 changed files with 169 additions and 25 deletions
+17 -23
View File
@@ -935,29 +935,23 @@ func (v *Vaultik) downloadManifestByKey(remoteKey string) (*snapshot.Manifest, e
func (v *Vaultik) syncWithRemote() error {
log.Info("Syncing with remote snapshots")
// Get all remote snapshot IDs
remoteSnapshots := make(map[string]bool)
objectCh := v.Storage.ListStream(v.ctx, "metadata/")
for object := range objectCh {
if object.Err != nil {
return fmt.Errorf("listing remote snapshots: %w", object.Err)
}
// Extract snapshot ID from paths like metadata/hostname-20240115-143052Z/
parts := strings.Split(object.Key, "/")
if len(parts) >= minSnapshotIDParts &&
parts[0] == metadataDirName && parts[1] != "" {
// Skip macOS resource fork files (._*) and other hidden files
if strings.HasPrefix(parts[1], ".") {
continue
}
remoteSnapshots[parts[1]] = true
}
// Remote metadata lives under metadata/<remote-key>/, where the
// directory name is snapshot.RemoteSnapshotKey(id), not the human
// snapshot ID. Compare each local row's hashed key against that set
// so a row still backed by remote metadata is kept. Comparing human
// IDs against the hashed directory names matches nothing and deletes
// every local snapshot record (issue #160).
remoteKeys, err := v.listAllRemoteSnapshotKeys()
if err != nil {
return fmt.Errorf("listing remote snapshots: %w", err)
}
log.Debug("Found remote snapshots", "count", len(remoteSnapshots))
remoteKeySet := make(map[string]bool, len(remoteKeys))
for _, k := range remoteKeys {
remoteKeySet[k] = true
}
log.Debug("Found remote snapshots", "count", len(remoteKeySet))
// Get all local snapshots (use a high limit to get all)
localSnapshots, err := v.Repositories.Snapshots.ListRecent(v.ctx, listRecentLimit)
@@ -965,12 +959,12 @@ func (v *Vaultik) syncWithRemote() error {
return fmt.Errorf("listing local snapshots: %w", err)
}
// Remove local snapshots that don't exist remotely
// Remove local snapshots whose metadata is absent from the remote.
removedCount := 0
for _, snap := range localSnapshots {
snapshotIDStr := snap.ID.String()
if !remoteSnapshots[snapshotIDStr] {
if !remoteKeySet[snapshot.RemoteSnapshotKey(snapshotIDStr)] {
log.Info("Removing local snapshot not found in remote",
"snapshot_id", snap.ID)