From 62b9b5acd468a5988bccb01625f7beb867e64ce7 Mon Sep 17 00:00:00 2001 From: midasmatterhorn <248507632+midasmatterhorn@users.noreply.github.com> Date: Tue, 27 Oct 2026 00:58:10 +0300 Subject: [PATCH 1/3] engine: save, render and export create a missing target folder instead of failing with os error 3 --- crates/engine/src/atomic_write.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/engine/src/atomic_write.rs b/crates/engine/src/atomic_write.rs index ff9e141d3..cd7d0fb4b 100644 --- a/crates/engine/src/atomic_write.rs +++ b/crates/engine/src/atomic_write.rs @@ -37,6 +37,9 @@ pub fn write_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> { Err(e) => return Err(e), }; let dir = path.parent().filter(|p| !p.as_os_str().is_empty()).map_or_else(|| PathBuf::from("."), Path::to_path_buf); + if let Err(e) = std::fs::create_dir_all(&dir) { + return Err(e); + } let (mut file, tmp) = create_temp(&dir, permissions.as_ref())?; let write = (|| { if let Some(permissions) = permissions { @@ -101,6 +104,15 @@ fn create_exclusive(path: &Path, permissions: Option<&Permissions>) -> io::Resul mod tests { use super::*; + #[test] + fn creates_missing_parent_directories() { + let dir = std::env::temp_dir().join(format!("cadcraft-aw-mkdir-{}-{}", std::process::id(), SEQ.fetch_add(1, Ordering::Relaxed))); + let dest = dir.join("new_folder").join("out.bin"); + write_atomic(&dest, b"created").unwrap(); + assert_eq!(std::fs::read(&dest).unwrap(), b"created"); + std::fs::remove_dir_all(&dir).ok(); + } + #[test] fn replaces_contents_atomically() { let dir = std::env::temp_dir().join(format!("cadcraft-aw-{}-{}", std::process::id(), SEQ.fetch_add(1, Ordering::Relaxed))); From c2cb5b86109b4df3a822a29ac0d706284e0c7b54 Mon Sep 17 00:00:00 2001 From: midasmatterhorn <236787783+midasmatterhorn@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:49:48 +0300 Subject: [PATCH 2/3] engine: propagate create_dir_all errors and route render PNG writes through write_atomic --- crates/engine/src/atomic_write.rs | 4 +--- crates/mcp/src/backend.rs | 3 ++- crates/ui-egui/src/control.rs | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/atomic_write.rs b/crates/engine/src/atomic_write.rs index cd7d0fb4b..2d05e019d 100644 --- a/crates/engine/src/atomic_write.rs +++ b/crates/engine/src/atomic_write.rs @@ -37,9 +37,7 @@ pub fn write_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> { Err(e) => return Err(e), }; let dir = path.parent().filter(|p| !p.as_os_str().is_empty()).map_or_else(|| PathBuf::from("."), Path::to_path_buf); - if let Err(e) = std::fs::create_dir_all(&dir) { - return Err(e); - } + std::fs::create_dir_all(&dir)?; let (mut file, tmp) = create_temp(&dir, permissions.as_ref())?; let write = (|| { if let Some(permissions) = permissions { diff --git a/crates/mcp/src/backend.rs b/crates/mcp/src/backend.rs index 9d012076a..d27012418 100644 --- a/crates/mcp/src/backend.rs +++ b/crates/mcp/src/backend.rs @@ -395,7 +395,8 @@ impl Backend for Headless { let png = cadcraft_render::raster::render_png(&list, &view, &cadcraft_render::raster::RasterOptions::default()).ok_or("render failed")?; if let Some(path) = str_p("path") { - std::fs::write(&path, &png).map_err(|e| format!("{path}: {e}"))?; + cadcraft_engine::atomic_write::write_atomic(std::path::Path::new(&path), &png) + .map_err(|e| format!("{path}: {e}"))?; } Ok(json!({"pngBase64": cadcraft_engine::cmd::file::base64_encode(&png), "width": w, "height": h})) } diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 2dc1f898a..4370b5de1 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -489,7 +489,7 @@ pub fn handle(app: &mut CadApp, ctx: &egui::Context, req: &ControlRequest) -> Ou }; #[cfg(not(target_arch = "wasm32"))] if let Some(path) = s("path") { - if let Err(e) = std::fs::write(path, &png) { + if let Err(e) = cadcraft_engine::atomic_write::write_atomic(std::path::Path::new(&path), &png) { return err(e); } return ok(render_reply(&png, w, h, Some(path))); From 9c84141e3899bd17a5747e578b11bcbf91ad28e1 Mon Sep 17 00:00:00 2001 From: midasmatterhorn <248507632+midasmatterhorn@users.noreply.github.com> Date: Sun, 1 Nov 2026 04:56:59 +0300 Subject: [PATCH 3/3] style: rustfmt the write_atomic map_err in render --- crates/mcp/src/backend.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/mcp/src/backend.rs b/crates/mcp/src/backend.rs index d27012418..4ed805979 100644 --- a/crates/mcp/src/backend.rs +++ b/crates/mcp/src/backend.rs @@ -395,8 +395,7 @@ impl Backend for Headless { let png = cadcraft_render::raster::render_png(&list, &view, &cadcraft_render::raster::RasterOptions::default()).ok_or("render failed")?; if let Some(path) = str_p("path") { - cadcraft_engine::atomic_write::write_atomic(std::path::Path::new(&path), &png) - .map_err(|e| format!("{path}: {e}"))?; + cadcraft_engine::atomic_write::write_atomic(std::path::Path::new(&path), &png).map_err(|e| format!("{path}: {e}"))?; } Ok(json!({"pngBase64": cadcraft_engine::cmd::file::base64_encode(&png), "width": w, "height": h})) }