mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-28 03:57:57 -06:00
LC-SAFETY: protect originals when exporting presets
This commit is contained in:
@@ -349,9 +349,8 @@ fn export_presets(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if chosen.is_empty() {
|
||||
return Err(bad(C, "no curve presets to export (save one first, or pass names)"));
|
||||
}
|
||||
if let Some(dir) = path.parent().filter(|d| !d.as_os_str().is_empty()) {
|
||||
std::fs::create_dir_all(dir).map_err(|e| crate::EngineError::Other(format!("{}: {e}", dir.display())))?;
|
||||
}
|
||||
std::fs::write(&path, to_file(&chosen)).map_err(|e| crate::EngineError::Other(format!("{}: {e}", path.display())))?;
|
||||
let target = path.to_string_lossy();
|
||||
s.check_write_target(&target).map_err(|e| bad(C, e))?;
|
||||
crate::export::write_file_durable(&target, to_file(&chosen).as_bytes()).map_err(crate::EngineError::Other)?;
|
||||
Ok(json!({"path": path.display().to_string(), "count": chosen.len()}))
|
||||
}
|
||||
|
||||
@@ -69,10 +69,9 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if chosen.is_empty() {
|
||||
return Err(bad("preset.export", "no presets to export (create one first, or pass ids/group)"));
|
||||
}
|
||||
if let Some(dir) = path.parent().filter(|d| !d.as_os_str().is_empty()) {
|
||||
std::fs::create_dir_all(dir).map_err(|e| crate::EngineError::Other(format!("{}: {e}", dir.display())))?;
|
||||
}
|
||||
std::fs::write(&path, to_lcpreset(&chosen)).map_err(|e| crate::EngineError::Other(format!("{}: {e}", path.display())))?;
|
||||
let target = path.to_string_lossy();
|
||||
s.check_write_target(&target).map_err(|e| bad("preset.export", e))?;
|
||||
crate::export::write_file_durable(&target, to_lcpreset(&chosen).as_bytes()).map_err(crate::EngineError::Other)?;
|
||||
Ok(json!({"path": path.display().to_string(), "count": chosen.len()}))
|
||||
}
|
||||
|
||||
|
||||
@@ -236,6 +236,69 @@ fn library_with_jpeg(tag: &str) -> (Session, lightcraft_catalog::PhotoId, std::p
|
||||
(s, id, dir, src, bytes)
|
||||
}
|
||||
|
||||
fn preset_export_preserves_originals(command: &str, selection: serde_json::Value, tag: &str) {
|
||||
let (mut s, _, dir, src, original) = library_with_jpeg(tag);
|
||||
let sidecar = src.with_extension("xmp");
|
||||
let appended_sidecar = dir.join("IMG_1.jpg.xmp");
|
||||
std::fs::write(&sidecar, b"existing sidecar").unwrap();
|
||||
std::fs::write(&appended_sidecar, b"appended sidecar").unwrap();
|
||||
std::fs::create_dir_all(dir.join("sub")).unwrap();
|
||||
let mut targets = vec![src.clone(), dir.join("sub/../IMG_1.jpg"), sidecar.clone()];
|
||||
targets.push(appended_sidecar.clone());
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let alias = dir.join("alias.jpg");
|
||||
std::os::unix::fs::symlink(&src, &alias).unwrap();
|
||||
targets.push(alias);
|
||||
let alias = dir.join("alias.xmp");
|
||||
std::os::unix::fs::symlink(&sidecar, &alias).unwrap();
|
||||
targets.push(alias);
|
||||
}
|
||||
for path in targets {
|
||||
let mut params = selection.clone();
|
||||
params["path"] = json!(path.to_string_lossy());
|
||||
let error = s.execute(command, ¶ms).unwrap_err().to_string();
|
||||
assert!(error.contains("never writes over an original"), "{error}");
|
||||
assert_eq!(std::fs::read(&src).unwrap(), original);
|
||||
assert_eq!(std::fs::read(&sidecar).unwrap(), b"existing sidecar");
|
||||
assert_eq!(std::fs::read(&appended_sidecar).unwrap(), b"appended sidecar");
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preset_export_never_overwrites_originals_or_sidecars() {
|
||||
preset_export_preserves_originals("preset.export", json!({"ids": ["lc.warm-glow"]}), "preset-export-guard");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curve_preset_export_never_overwrites_originals_or_sidecars() {
|
||||
preset_export_preserves_originals("curve.exportPresets", json!({"names": ["Linear"]}), "curve-export-guard");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preset_exports_preserve_previous_files_on_write_failure() {
|
||||
let dir = temp_dir("preset-export-failure");
|
||||
let mut s = Session::new();
|
||||
for (command, selection, file) in
|
||||
[("preset.export", json!({"ids": ["lc.warm-glow"]}), "look.lcpreset"), ("curve.exportPresets", json!({"names": ["Linear"]}), "curve.lccurve")]
|
||||
{
|
||||
let path = dir.join(file);
|
||||
let mut params = selection;
|
||||
params["path"] = json!(path.to_string_lossy());
|
||||
s.execute(command, ¶ms).unwrap();
|
||||
let previous = std::fs::read(&path).unwrap();
|
||||
{
|
||||
let _fault = lightcraft_catalog::safe_file::fail_writes_after(4);
|
||||
assert!(s.execute(command, ¶ms).is_err(), "{command} must report the failed write");
|
||||
}
|
||||
assert_eq!(std::fs::read(&path).unwrap(), previous, "{command} preserves the prior export");
|
||||
assert_eq!(s.execute(command, ¶ms).unwrap()["count"], 1, "replacement still succeeds");
|
||||
}
|
||||
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 2, "no temporary files remain");
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
fn disk_batch(
|
||||
s: &mut Session,
|
||||
id: lightcraft_catalog::PhotoId,
|
||||
|
||||
Reference in New Issue
Block a user