mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
io: remove the .sctmp temporary file when a save fails
write_file writes `<path>.sctmp` and renames it over the workbook. When the write or the rename failed (a full disk, or the target locked by another app such as Excel or a sync client), the temporary file was left beside the user's workbook. Remove it on either failure and report the same error. Adds io::tests::failed_save_leaves_no_temporary_file, which fails before this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3926d21aa9
commit
3424229ee3
+26
-2
@@ -339,8 +339,12 @@ pub fn read_file(path: &str) -> Result<Vec<u8>> {
|
||||
pub fn write_file(path: &str, bytes: &[u8]) -> Result<()> {
|
||||
// Write to a temporary file, then rename: a crash never leaves a half-written workbook.
|
||||
let tmp = format!("{path}.sctmp");
|
||||
std::fs::write(&tmp, bytes).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
std::fs::rename(&tmp, path).map_err(|e| EngineError::Other(format!("{path}: {e}")))
|
||||
std::fs::write(&tmp, bytes).and_then(|()| std::fs::rename(&tmp, path)).map_err(|e| {
|
||||
// A write or rename that fails (full disk, the workbook locked by another app) leaves
|
||||
// no temporary file beside the user's workbook.
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
EngineError::Other(format!("{path}: {e}"))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -434,6 +438,26 @@ mod tests {
|
||||
assert!(wb.vba_project.is_some(), "the open workbook keeps its macros");
|
||||
}
|
||||
|
||||
/// A save that can't be put in place (here a folder already has the workbook's name, as
|
||||
/// with a file another app holds locked) fails without leaving `<name>.sctmp` behind.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn failed_save_leaves_no_temporary_file() {
|
||||
let dir = std::env::temp_dir().join(format!("gridcraft-write-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let blocked = dir.join("Book1.xlsx");
|
||||
std::fs::create_dir(&blocked).unwrap();
|
||||
assert!(write_file(blocked.to_str().unwrap(), b"workbook").is_err());
|
||||
assert!(!dir.join("Book1.xlsx.sctmp").exists(), "the temporary file is removed");
|
||||
// An ordinary save still works and leaves only the workbook.
|
||||
let ok = dir.join("Book2.xlsx");
|
||||
write_file(ok.to_str().unwrap(), b"workbook").unwrap();
|
||||
assert_eq!(std::fs::read(&ok).unwrap(), b"workbook");
|
||||
assert!(!dir.join("Book2.xlsx.sctmp").exists());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A minimal compound (CFB/OLE2) file — FAT in sector 0, directory in sector 1 — holding a
|
||||
/// root entry and the given stream names.
|
||||
fn cfb(streams: &[&str]) -> Vec<u8> {
|
||||
|
||||
Reference in New Issue
Block a user