Reconcile purge against remote by hashed key, not human ID (closes #160)
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:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user