agent: code-atomiser-fix — paths.rs multi-legacy-candidate migration + copy_dir_recursive symlink loop
Two reversibility defects in `crates/core/src/paths.rs`: Defect A (multi-legacy-candidate orphan): `resolve_app_data_dir` and `legacy_and_target_paths` short-circuited on the first legacy candidate, allowing two reachable orphan scenarios on Linux. With both `~/.magnotia` and `~/.local/share/magnotia` the shim migrated only the dot-home variant, leaving the XDG legacy invisible forever. With a stray `~/.lumotia` alongside a freshly migrated `~/.local/share/lumotia`, the resolver kept returning the dot-home path, orphaning the XDG target. `legacy_and_target_paths` now returns `Vec<(legacy, target)>`, probing every legacy variant the platform supports. The migration driver in `src-tauri/src/lib.rs` loops over the Vec and emits per-candidate tracing. A new `resolve_app_data_dir_strict` + `check_target_ambiguity` API refuses to start when more than one target candidate exists post-migration, surfacing both paths to the user via the setup hook instead of silently picking one. Regression tests: `migrate_handles_both_dot_home_and_xdg`, `resolve_app_data_dir_refuses_on_multiple_targets`. Defect B (copy_dir_recursive symlink loop on EXDEV migration): `entry.metadata()` follows symlinks, so a directory symlink reported is_dir==true and recursed unconditionally. A self-referential or ancestor-targeting directory symlink would loop until the disk filled. Switched to `entry.file_type()` (symlink-aware), re-ordered branches so `is_symlink()` is checked first, and routed all symlinks through symlink-creation (Unix + Windows) rather than recursive copy. Regression tests: `copy_dir_recursive_does_not_loop_on_self_referential_dir_symlink`, `copy_dir_recursive_preserves_directory_symlinks`. 14/14 paths tests green. Full workspace cargo test green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -234,16 +234,28 @@ pub fn run() {
|
||||
// settings behind a fresh empty lumotia dir.
|
||||
let t_migrate = Instant::now();
|
||||
match migrate_legacy_data_dir() {
|
||||
Ok(MigrationStatus::Migrated { from, to, renamed_db }) => tracing::info!(
|
||||
target: "lumotia_startup",
|
||||
elapsed_ms = t_migrate.elapsed().as_millis(),
|
||||
from = %from.display(),
|
||||
to = %to.display(),
|
||||
renamed_db,
|
||||
"migrated legacy magnotia data dir to lumotia"
|
||||
),
|
||||
Ok(MigrationStatus::TargetAlreadyExists { .. }) => {}
|
||||
Ok(MigrationStatus::NoLegacyFound) => {}
|
||||
Ok(statuses) => {
|
||||
// Drive every legacy candidate independently: on Linux a
|
||||
// user may have both `~/.magnotia` and
|
||||
// `~/.local/share/magnotia`, and migrating only one
|
||||
// would orphan the other forever.
|
||||
for status in &statuses {
|
||||
match status {
|
||||
MigrationStatus::Migrated { from, to, renamed_db } => {
|
||||
tracing::info!(
|
||||
target: "lumotia_startup",
|
||||
elapsed_ms = t_migrate.elapsed().as_millis(),
|
||||
from = %from.display(),
|
||||
to = %to.display(),
|
||||
renamed_db = *renamed_db,
|
||||
"migrated legacy magnotia data dir to lumotia"
|
||||
);
|
||||
}
|
||||
MigrationStatus::TargetAlreadyExists { .. } => {}
|
||||
MigrationStatus::NoLegacyFound => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
target: "lumotia_startup",
|
||||
@@ -254,6 +266,19 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
|
||||
// After migration, refuse to start if more than one lumotia
|
||||
// target candidate exists on disk (e.g. both `~/.lumotia` AND
|
||||
// `~/.local/share/lumotia`). Silently picking one would point
|
||||
// the app at the wrong half of a split data directory.
|
||||
if let Err(amb) = check_target_ambiguity() {
|
||||
tracing::error!(
|
||||
target: "lumotia_startup",
|
||||
error = %amb,
|
||||
"ambiguous lumotia data directory — refusing to start"
|
||||
);
|
||||
return Err(Box::new(amb) as Box<dyn std::error::Error>);
|
||||
}
|
||||
|
||||
// Initialise database and startup settings in one runtime entry.
|
||||
let db_path = database_path();
|
||||
let (db, init_script) = tauri::async_runtime::block_on(async {
|
||||
|
||||
Reference in New Issue
Block a user