From a9214db3443164a9acc808f50f85bb718cb889fa Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 29 Jun 2026 16:39:42 +0300 Subject: [PATCH] fix(autosave): autosave unsaved drawings + clean up .sv$ on file-close (#205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps in the autosave recovery files: 1. A brand-new, never-saved drawing was never autosaved — on_autosave only wrote tabs that already had a path. A shared autosave_target() now writes saved drawings to .sv$ and unsaved ones to the system temp dir (OpenCADStudio_.sv$). 2. Closing a file (tab) left its .sv$ behind, which also defeated the program-close cleanup (it only scans still-open tabs). on_tab_close and the unsaved-discard path now delete the closing tab's .sv$, and Save/Save-As drop the temp copy a drawing used while it was still unsaved. Program-close cleanup (exit_app -> cleanup_autosaves) already worked. Co-Authored-By: Claude Opus 4.8 --- src/app/update/command.rs | 3 +++ src/app/update/dialog.rs | 7 +++++++ src/app/update/file.rs | 42 +++++++++++++++++++++++++++------------ 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 0c2f34c41..bc0476904 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -31,6 +31,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { self.pending_close = Some(crate::app::PendingClose::Tab(idx)); return self.open_unsaved_dialog_window(); } + // This tab is closing for good — drop its autosave recovery copy. + #[cfg(not(target_arch = "wasm32"))] + let _ = std::fs::remove_file(self.autosave_target(idx)); // Only-tab case: when the lone non-start tab closes, fall // back to the Start tab if it exists; otherwise spawn a // fresh blank drawing (legacy behaviour). diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index 0ed6eefe3..23c356533 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -157,6 +157,9 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven match self.pending_close.take() { Some(crate::app::PendingClose::Tab(idx)) => { let close_win = self.close_unsaved_dialog_window(); + // Discarded — drop this tab's autosave recovery copy. + #[cfg(not(target_arch = "wasm32"))] + let _ = std::fs::remove_file(self.autosave_target(idx)); if self.tabs.len() == 1 { self.tab_counter += 1; self.tabs[0] = @@ -272,6 +275,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven Ok(()) => { self.command_line .push_output(&format!("Saved: {}", path.display())); + #[cfg(not(target_arch = "wasm32"))] + let _ = std::fs::remove_file(self.autosave_target(idx)); self.tabs[idx].current_path = Some(path); self.tabs[idx].dirty = false; return self.update(Message::TabClose(idx)); @@ -293,6 +298,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven Ok(()) => { self.command_line .push_output(&format!("Saved: {}", path.display())); + #[cfg(not(target_arch = "wasm32"))] + let _ = std::fs::remove_file(self.autosave_target(i)); self.tabs[i].current_path = Some(path); self.tabs[i].dirty = false; if self.tabs.iter().any(|t| t.dirty) { diff --git a/src/app/update/file.rs b/src/app/update/file.rs index ed14dba89..bfddcd604 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -615,9 +615,12 @@ pub(super) fn on_open_file(&mut self) -> Task { Ok(()) => { self.command_line .push_output(&format!("Saved: {}", path.display())); + // Drop any prior autosave copy — including the temp + // one used while the drawing was still unsaved — + // before the tab takes on its new path. + let _ = std::fs::remove_file(self.autosave_target(i)); self.tabs[i].current_path = Some(path.clone()); self.tabs[i].dirty = false; - // A clean save supersedes any autosave recovery copy. let _ = std::fs::remove_file(path.with_extension("sv$")); if self.save_dialog_for_unsaved { let next = self.update(Message::UnsavedPickedSavePath(Some(path))); @@ -681,10 +684,28 @@ pub(super) fn on_open_file(&mut self) -> Task { self.on_save_dialog_confirm() } + /// Where the autosave recovery copy for tab `i` lives: beside a saved + /// drawing as `.sv$`, or — for an unsaved drawing with no path yet — + /// under the system temp dir keyed by the tab's display name. + #[cfg(not(target_arch = "wasm32"))] + pub(in crate::app) fn autosave_target(&self, i: usize) -> std::path::PathBuf { + match &self.tabs[i].current_path { + Some(p) => p.with_extension("sv$"), + None => { + let safe: String = self.tabs[i] + .tab_display_name() + .chars() + .map(|c| if c.is_alphanumeric() { c } else { '_' }) + .collect(); + std::env::temp_dir().join(format!("OpenCADStudio_{safe}.sv$")) + } + } + } + /// Periodic autosave (SAVETIME): write a `.sv$` recovery copy for every - /// dirty tab that has a path, at the document's own DWG version. Best-effort - /// and non-destructive — it never touches the original file or the dirty - /// flag, and skips unsaved (path-less) tabs. + /// dirty tab — beside the file if it's saved, else under the temp dir — at + /// the document's own DWG version. Best-effort and non-destructive: it never + /// touches the original file or the dirty flag. #[cfg(not(target_arch = "wasm32"))] pub(super) fn on_autosave(&mut self) -> Task { let mut n = 0; @@ -692,21 +713,18 @@ pub(super) fn on_open_file(&mut self) -> Task { if !self.tabs[i].dirty { continue; } - let Some(path) = self.tabs[i].current_path.clone() else { - continue; - }; let version = self.tabs[i].scene.document.version; if let Ok(bytes) = crate::io::save_to_bytes(&self.tabs[i].scene.document, "dwg", version) { - if std::fs::write(path.with_extension("sv$"), bytes).is_ok() { + if std::fs::write(self.autosave_target(i), bytes).is_ok() { n += 1; } } } if n > 0 { self.command_line - .push_output(&format!("Autosaved {n} drawing(s) to .sv$")); + .push_output(&format!("Autosaved {n} drawing(s)")); } Task::none() } @@ -720,10 +738,8 @@ pub(super) fn on_open_file(&mut self) -> Task { /// exist only to survive a crash, so a clean save or exit removes them. pub(in crate::app) fn cleanup_autosaves(&self) { #[cfg(not(target_arch = "wasm32"))] - for t in &self.tabs { - if let Some(path) = &t.current_path { - let _ = std::fs::remove_file(path.with_extension("sv$")); - } + for i in 0..self.tabs.len() { + let _ = std::fs::remove_file(self.autosave_target(i)); } }