fix(io): guard concurrent drawing saves

Hold edit leases and compare disk fingerprints before replacement so another editor cannot be silently overwritten.

Offer retry, reload, Save As, or explicit overwrite recovery.

Closes #498
This commit is contained in:
Hakan Seven 2026-07-27 13:13:58 +03:00
commit 2acc4f7978
12 changed files with 1424 additions and 23 deletions

View file

@ -80,6 +80,16 @@ impl OpenCADStudio {
if self.active_modal == Some(ScaleManager) {
self.scale_stage_discard();
}
#[cfg(not(target_arch = "wasm32"))]
if self.active_modal == Some(FileInUse) {
self.pending_save_failure = None;
self.pending_close = None;
}
#[cfg(not(target_arch = "wasm32"))]
if self.active_modal == Some(ExternalChange) {
self.pending_external_change = None;
self.pending_close = None;
}
match self.active_modal {
// Dismissing these via ✕ is the cancel/decline path.
Some(Unsaved) => self.pending_close = None,
@ -457,6 +467,9 @@ impl OpenCADStudio {
size_bytes,
state: progress.clone(),
started: Instant::now(),
#[cfg(not(target_arch = "wasm32"))]
fingerprint:
crate::io::edit_lock::FileFingerprint::capture(&path).ok(),
});
let size_label = format_size(size_bytes);
self.command_line
@ -4067,6 +4080,33 @@ impl OpenCADStudio {
#[cfg(not(target_arch = "wasm32"))]
Message::SaveFinished(outcome) => self.on_save_finished(outcome),
#[cfg(not(target_arch = "wasm32"))]
Message::SaveFileInUseRetry => self.on_save_file_in_use_retry(),
#[cfg(not(target_arch = "wasm32"))]
Message::SaveFileInUseSaveAs => self.on_save_file_in_use_save_as(),
#[cfg(not(target_arch = "wasm32"))]
Message::SaveFileInUseCancel => {
self.close_active_modal();
Task::none()
}
#[cfg(not(target_arch = "wasm32"))]
Message::ExternalChangeReload => self.on_external_change_reload(),
#[cfg(not(target_arch = "wasm32"))]
Message::ExternalChangeSaveAs => self.on_external_change_save_as(),
#[cfg(not(target_arch = "wasm32"))]
Message::ExternalChangeOverwrite => self.on_external_change_overwrite(),
#[cfg(not(target_arch = "wasm32"))]
Message::ExternalChangeCancel => {
self.close_active_modal();
Task::none()
}
// ── Page Setup ────────────────────────────────────────────────
Message::UpdateCheckResult(latest) => {
let Some(info) = latest else {