Merge #2260: a save to a missing folder writes the file and says so

The folders are made by a new writer only user-named targets use, so autosave
and a dangling symlink's target keep the strict write_atomic. Save, Save As,
Export, Plot, WBLOCK, layout templates, render and the CLI report the topmost
new folder at the command line and in createdFolder.
This commit is contained in:
Tanish Manku
2026-11-02 16:48:16 -07:00
11 changed files with 187 additions and 17 deletions
+6 -2
View File
@@ -8,7 +8,7 @@ use cadcraft_geom::{Mat3, Vec2, Vec3};
use serde_json::{Value, json};
use super::super::curves::KW;
use super::super::file::{drops_unknown, not_saved, not_saved_message, report_not_saved};
use super::super::file::{drops_unknown, new_folder, not_saved, not_saved_message, report_new_folder, report_not_saved};
use super::super::machines::{SelOutcome, SelectPhase};
use super::super::modify::erase_kept;
use super::super::{CommandSpec, bad, point_param, str_param, targets};
@@ -43,11 +43,13 @@ enum Source<'a> {
fn run_wblock(s: &mut Session, p: &Value) -> Result<Value> {
let path = str_param(p, "path").ok_or_else(|| bad("wblock", "`path` is required"))?.to_string();
let base = point_param(p, "base");
let folder = new_folder(&path);
let (entities, bytes, lost) = match str_param(p, "name") {
Some(name) => write(s, "wblock", &path, Source::Block(name, base))?,
None => write(s, "wblock", &path, Source::Objects(&targets(s, p)?, base.unwrap_or(Vec2::ZERO)))?,
};
let mut r = json!({ "path": path, "entities": entities, "bytes": bytes });
report_new_folder(s, &mut r, folder);
report_not_saved(s, &mut r, &lost);
Ok(r)
}
@@ -58,6 +60,7 @@ fn run_dash_wblock(s: &mut Session, p: &Value) -> Result<Value> {
}
let path = str_param(p, "path").ok_or_else(|| bad("-wblock", "`path` is required"))?.to_string();
let base = point_param(p, "base");
let folder = new_folder(&path);
let (entities, bytes, lost) = match str_param(p, "name") {
Some(name) => write(s, "-wblock", &path, Source::Block(name, base))?,
None => {
@@ -72,6 +75,7 @@ fn run_dash_wblock(s: &mut Session, p: &Value) -> Result<Value> {
}
};
let mut r = json!({ "path": path, "entities": entities, "bytes": bytes });
report_new_folder(s, &mut r, folder);
report_not_saved(s, &mut r, &lost);
Ok(r)
}
@@ -101,7 +105,7 @@ fn write(s: &Session, cmd: &str, path: &str, what: Source) -> Result<(usize, usi
let bytes = (hooks.write)(&d, path).map_err(|e| bad(cmd, e))?;
// Through a temporary file, as SAVE writes, so a failed write keeps the file it replaces.
#[cfg(not(target_arch = "wasm32"))]
crate::atomic_write::write_atomic(std::path::Path::new(path), &bytes).map_err(|e| bad(cmd, format!("{path}: {e}")))?;
crate::atomic_write::write_atomic_making_folders(std::path::Path::new(path), &bytes).map_err(|e| bad(cmd, format!("{path}: {e}")))?;
let lost = if drops_unknown(path) { not_saved(&d) } else { BTreeMap::new() };
Ok((d.model.len(), bytes.len(), lost))
}
+3 -1
View File
@@ -82,7 +82,7 @@ fn write_file(cmd: &str, path: &str, bytes: &[u8]) -> Result<()> {
{
// Exclusively create a fresh temporary file and rename it into place so a planted
// `<path>.cadcraft-tmp` symlink cannot redirect the export write (CWE-59).
crate::atomic_write::write_atomic(std::path::Path::new(path), bytes).map_err(|e| bad(cmd, format!("{path}: {e}")))
crate::atomic_write::write_atomic_making_folders(std::path::Path::new(path), bytes).map_err(|e| bad(cmd, format!("{path}: {e}")))
}
#[cfg(target_arch = "wasm32")]
{
@@ -119,6 +119,7 @@ fn run_export(s: &mut Session, p: &Value) -> Result<Value> {
}
let file = format!("export.{format}");
let bytes = (hooks.write)(s.doc()?, &file).map_err(|e| bad("export", e))?;
let folder = path.and_then(super::file::new_folder);
let mut r = match path {
Some(path) => {
write_file("export", path, &bytes)?;
@@ -126,6 +127,7 @@ fn run_export(s: &mut Session, p: &Value) -> Result<Value> {
}
None => json!({ "data": base64_encode(&bytes), "format": format, "bytes": bytes.len() }),
};
super::file::report_new_folder(s, &mut r, folder);
super::file::report_loss(s, &mut r, &file);
Ok(r)
}
+25 -1
View File
@@ -446,7 +446,7 @@ fn store(path: &str, bytes: &[u8]) -> Result<()> {
}
// Exclusively create a fresh temporary file and rename it over `path`, so a planted
// `<path>.cadcraft-tmp` symlink cannot redirect the write onto another file (CWE-59).
crate::atomic_write::write_atomic(std::path::Path::new(path), bytes).map_err(|e| bad("save", format!("{path}: {e}")))
crate::atomic_write::write_atomic_making_folders(std::path::Path::new(path), bytes).map_err(|e| bad("save", format!("{path}: {e}")))
}
#[cfg(target_arch = "wasm32")]
{
@@ -851,6 +851,28 @@ fn report_sat_text_dropped(s: &mut Session, r: &mut Value) {
}
}
/// The folder a write to `path` has to create, asked before writing so it can be reported after.
/// The web has no folders.
pub(super) fn new_folder(path: &str) -> Option<String> {
#[cfg(not(target_arch = "wasm32"))]
return crate::atomic_write::missing_folder(std::path::Path::new(path)).map(|f| f.display().to_string());
#[cfg(target_arch = "wasm32")]
{
let _ = path;
None
}
}
/// Say on the command line that the file went into a folder that didn't exist, so a mistyped path
/// is noticed, and name the folder in the result's `createdFolder`.
pub(super) fn report_new_folder(s: &mut Session, r: &mut Value, folder: Option<String>) {
let Some(folder) = folder else { return };
s.echo(format!("Created the folder {folder}, which did not exist. Check the path if you did not mean to."));
if let Some(o) = r.as_object_mut() {
o.insert("createdFolder".into(), json!(folder));
}
}
/// Say on the command line what the file couldn't keep, and list it in the result's `notSaved`.
pub(super) fn report_not_saved(s: &mut Session, r: &mut Value, lost: &std::collections::BTreeMap<String, usize>) {
let Some(msg) = not_saved_message(lost) else { return };
@@ -867,7 +889,9 @@ fn save_reporting(s: &mut Session, p: &Value, run: fn(&mut Session, &Value) -> R
let path = str_param(p, "path").map(str::to_string).or_else(|| s.state().ok().and_then(|st| st.path.clone())).unwrap_or_default();
let version = asked.unwrap_or_else(|| drawing_version(s, &path));
let lost = s.doc().map(|d| not_saved_as(d, version)).unwrap_or_default();
let folder = new_folder(&path);
let mut r = run(s, p)?;
report_new_folder(s, &mut r, folder);
if r.get("path").and_then(Value::as_str).is_some_and(drops_unknown) {
report_not_saved(s, &mut r, &lost);
} else if version >= Version::R2013 && r.get("path").and_then(Value::as_str).is_some_and(is_dxf_file) {
+5 -2
View File
@@ -840,8 +840,11 @@ fn run_plot(s: &mut Session, p: &Value, cmd: &str) -> Result<Value> {
let path = _path;
// Exclusively create a fresh temporary file and rename it into place so a planted
// `<path>.cadcraft-tmp` symlink cannot redirect the plot write (CWE-59).
crate::atomic_write::write_atomic(std::path::Path::new(path), &bytes).map_err(|e| bad(cmd, format!("{path}: {e}")))?;
Ok(json!({ "path": path, "bytes": bytes.len(), "layout": layout, "pages": pages }))
let folder = super::file::new_folder(path);
crate::atomic_write::write_atomic_making_folders(std::path::Path::new(path), &bytes).map_err(|e| bad(cmd, format!("{path}: {e}")))?;
let mut r = json!({ "path": path, "bytes": bytes.len(), "layout": layout, "pages": pages });
super::file::report_new_folder(s, &mut r, folder);
Ok(r)
}
#[cfg(target_arch = "wasm32")]
{
+3 -1
View File
@@ -104,8 +104,10 @@ pub(super) fn run_saveas(s: &mut Session, p: &Value) -> Result<Value> {
let t = template_of(d, &name)?;
let hooks = io().ok_or_else(|| bad("layout.saveas", "file formats are not available in this build"))?;
let bytes = (hooks.write)(&t, &path).map_err(|e| bad("layout.saveas", e))?;
let folder = super::super::file::new_folder(&path);
store(&path, &bytes)?;
let mut r = json!({ "name": name, "path": path, "bytes": bytes.len() });
super::super::file::report_new_folder(s, &mut r, folder);
let lost = not_saved(&t);
if drops_unknown(&path)
&& let Some(msg) = not_saved_message(&lost)
@@ -124,7 +126,7 @@ fn store(path: &str, bytes: &[u8]) -> Result<()> {
{
// Exclusively create a fresh temporary file and rename it into place so a planted
// `<path>.cadcraft-tmp` symlink cannot redirect the write (CWE-59).
crate::atomic_write::write_atomic(std::path::Path::new(path), bytes).map_err(|e| bad("layout.saveas", format!("{path}: {e}")))
crate::atomic_write::write_atomic_making_folders(std::path::Path::new(path), bytes).map_err(|e| bad("layout.saveas", format!("{path}: {e}")))
}
#[cfg(target_arch = "wasm32")]
{