mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
fix(sync): restore an old install's backup before deciding to replace
A pre-0.3.3 install interrupted between its renames left only a backup. Restoring it inside the install step came after the decision, so an older snapshot replaced a newer database without --force, and a failed download left no database (Pullfrog review on #54). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
parent
b472d76e27
commit
94aec74b09
2 changed files with 41 additions and 42 deletions
|
|
@ -1924,12 +1924,24 @@ fn ownership_sync_installs_snapshot_and_preserves_query_behavior() {
|
|||
}
|
||||
|
||||
let bytes = fs::read(&target_db).unwrap();
|
||||
// A pre-0.3.3 install interrupted between its two renames left only a
|
||||
// backup. Sync restores it before deciding, so it is kept like any local
|
||||
// database (no_change, not a reinstall), and survives a failed download.
|
||||
let backup = sync_root.join(".ownership.db.sync-backup");
|
||||
fs::rename(&target_db, &backup).unwrap();
|
||||
let result =
|
||||
run_success_json(bin_with_root(&sync_root).args(["-o", "json", "ownership", "sync"]));
|
||||
assert_eq!(result["action"], "no_change");
|
||||
assert_eq!(fs::read(&target_db).unwrap(), bytes);
|
||||
assert!(!backup.exists());
|
||||
|
||||
// Both failures happen after a usable local snapshot has been installed.
|
||||
write_snapshot_manifest(
|
||||
&manifest_path,
|
||||
&source_db,
|
||||
Some("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"),
|
||||
);
|
||||
fs::rename(&target_db, &backup).unwrap();
|
||||
run_error_json(
|
||||
bin_with_root(&sync_root).args(["-o", "json", "ownership", "sync", "--force"]),
|
||||
"PARSEERROR",
|
||||
|
|
|
|||
|
|
@ -161,6 +161,10 @@ pub fn sync_snapshot<L>(
|
|||
let resolved_db_path = db::resolve_db_path(db_path)?;
|
||||
let db_path = resolved_db_path.as_path();
|
||||
let manifest = fetch_manifest(manifest_source)?;
|
||||
{
|
||||
let _lock = lock(db_path, true)?;
|
||||
recover_interrupted_install(db_path)?;
|
||||
}
|
||||
let local_state = {
|
||||
let _shared = lock(db_path, false)?;
|
||||
inspect_local_db(db_path)?
|
||||
|
|
@ -691,24 +695,6 @@ fn install_snapshot_file(temp_path: &Path, db_path: &Path) -> Result<(), IdxErro
|
|||
})?;
|
||||
}
|
||||
|
||||
// Versions before 0.3.3 moved the database to a backup before installing;
|
||||
// a crash between the two renames left only the backup. Restore it, or
|
||||
// drop it when the database is there.
|
||||
let backup_path = build_backup_path(db_path);
|
||||
if backup_path.exists() {
|
||||
let result = if db_path.exists() {
|
||||
fs::remove_file(&backup_path)
|
||||
} else {
|
||||
fs::rename(&backup_path, db_path)
|
||||
};
|
||||
result.map_err(|e| {
|
||||
IdxError::Io(format!(
|
||||
"failed to resolve leftover backup {}: {e}",
|
||||
backup_path.display()
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
// One rename replaces the old database atomically: a crash leaves either
|
||||
// the old file or the new one, never neither. Queries do not write to a
|
||||
// synced file, so its sidecars hold no data and go first.
|
||||
|
|
@ -724,6 +710,29 @@ fn install_snapshot_file(temp_path: &Path, db_path: &Path) -> Result<(), IdxErro
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Versions before 0.3.3 moved the database to a backup before installing;
|
||||
/// a crash between the two renames left only the backup. Restore it, or drop
|
||||
/// it when the database is there. Runs before the local database is
|
||||
/// inspected, so the restored one gets the usual newer/modified safeguards
|
||||
/// and stays in place if the download then fails.
|
||||
fn recover_interrupted_install(db_path: &Path) -> Result<(), IdxError> {
|
||||
let backup_path = build_backup_path(db_path);
|
||||
if !backup_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let result = if db_path.exists() {
|
||||
fs::remove_file(&backup_path)
|
||||
} else {
|
||||
fs::rename(&backup_path, db_path)
|
||||
};
|
||||
result.map_err(|e| {
|
||||
IdxError::Io(format!(
|
||||
"failed to resolve leftover backup {}: {e}",
|
||||
backup_path.display()
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn build_backup_path(db_path: &Path) -> PathBuf {
|
||||
let file_name = db_path
|
||||
.file_name()
|
||||
|
|
@ -882,8 +891,8 @@ mod tests {
|
|||
use super::{
|
||||
DEFAULT_SNAPSHOT_MANIFEST_URL, OwnershipSnapshotArtifact, OwnershipSnapshotManifest,
|
||||
OwnershipSnapshotSource, OwnershipSyncAction, SNAPSHOT_MANIFEST_SCHEMA_VERSION,
|
||||
build_backup_path, build_sync_decision, check_download_source, install_snapshot_file,
|
||||
parse_manifest, read_local_bounded, resolve_manifest_source_with, validate_snapshot_db,
|
||||
build_sync_decision, check_download_source, parse_manifest, read_local_bounded,
|
||||
resolve_manifest_source_with, validate_snapshot_db,
|
||||
};
|
||||
use crate::ownership::db::{ensure_schema, insert_release};
|
||||
use crate::ownership::types::OwnershipRelease;
|
||||
|
|
@ -1107,28 +1116,6 @@ mod tests {
|
|||
assert!(err.to_string().contains("not a regular file"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_restores_a_backup_left_by_an_interrupted_older_install() {
|
||||
let dir = std::env::temp_dir().join(format!("idx-install-{}", fastrand::u64(..)));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let db = dir.join("ownership.db");
|
||||
// An interrupted pre-0.3.3 install left only the backup. Even if this
|
||||
// install then fails (its temp file is gone), the old data comes back
|
||||
// instead of being deleted, as the old code did first.
|
||||
std::fs::write(build_backup_path(&db), b"previous").unwrap();
|
||||
let missing_temp = dir.join("incoming.tmp");
|
||||
|
||||
assert!(install_snapshot_file(&missing_temp, &db).is_err());
|
||||
assert_eq!(std::fs::read(&db).unwrap(), b"previous");
|
||||
|
||||
let temp = dir.join("incoming-2.tmp");
|
||||
std::fs::write(&temp, b"snapshot").unwrap();
|
||||
install_snapshot_file(&temp, &db).unwrap();
|
||||
assert_eq!(std::fs::read(&db).unwrap(), b"snapshot");
|
||||
assert!(!build_backup_path(&db).exists());
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_manifests_over_the_limit_are_rejected_not_truncated() {
|
||||
let path = std::env::temp_dir().join(format!("idx-manifest-{}.json", fastrand::u64(..)));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue