mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-28 20:36:55 -06:00
Merge pull request #1314 from denisg0mes/keep-images-past-budget
M7: don't save over a file whose images past the budget weren't loaded
This commit is contained in:
@@ -163,7 +163,10 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
let i = s.active;
|
||||
let uid = s.ds()?.uid;
|
||||
if let Some(slot) = s.docs.get_mut(i) {
|
||||
*slot = DocState::new(loaded.doc, Some(path)).with_versions(loaded.versions).with_branches(loaded.branches);
|
||||
*slot = DocState::new(loaded.doc, Some(path.clone()))
|
||||
.with_versions(loaded.versions)
|
||||
.with_branches(loaded.branches)
|
||||
.with_images_left_in_file(&path, loaded.images_past_budget);
|
||||
}
|
||||
// Edits on its branch tabs are reverted too.
|
||||
crate::branches::close_tabs(s, uid, None);
|
||||
@@ -738,7 +741,13 @@ fn open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
});
|
||||
}
|
||||
let summary = import_summary(&loaded.doc);
|
||||
s.docs.push(DocState::new(loaded.doc, native.then_some(path)).with_versions(loaded.versions).with_branches(loaded.branches));
|
||||
let images_left_out = if native { loaded.images_past_budget } else { 0 };
|
||||
s.docs.push(
|
||||
DocState::new(loaded.doc, native.then(|| path.clone()))
|
||||
.with_versions(loaded.versions)
|
||||
.with_branches(loaded.branches)
|
||||
.with_images_left_in_file(&path, images_left_out),
|
||||
);
|
||||
s.active = s.docs.len() - 1;
|
||||
let mut out = json!({"index": s.active, "summary": summary});
|
||||
out[if warnings { "warnings" } else { "repairs" }] = json!(loaded.issues);
|
||||
@@ -826,7 +835,9 @@ impl FileSnapshot {
|
||||
let Some(ds) = s.docs.iter_mut().find(|d| d.uid == self.uid && d.path == self.path && Arc::ptr_eq(&d.saved_doc, &self.saved)) else {
|
||||
return Ok(false);
|
||||
};
|
||||
write(self.path.as_deref().ok_or(EngineError::NoDocument)?, bytes)?;
|
||||
let path = self.path.as_deref().ok_or(EngineError::NoDocument)?;
|
||||
refuse_dropping_images(ds, path)?;
|
||||
write(path, bytes)?;
|
||||
// The history or branches may have changed since: then they still count as unsaved.
|
||||
if ds.revision == self.revision {
|
||||
ds.versions_changed = false;
|
||||
@@ -838,6 +849,16 @@ impl FileSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
fn refuse_dropping_images(ds: &DocState, path: &str) -> Result<()> {
|
||||
match ds.images_lost_saving_to(path) {
|
||||
Some(count) => Err(EngineError::Other(format!(
|
||||
"{} wasn't saved: {count} of its images weren't loaded (together they pass what FigCraft reads from one file), and saving over it would delete them. Save it under another name; this file keeps them.",
|
||||
file_stem(path)
|
||||
))),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The branch tabs of the file in tab `uid` were saved with it.
|
||||
fn branch_tabs_saved(s: &mut Session, uid: u64) {
|
||||
for tab in s.docs.iter_mut().filter(|d| d.branch_of.as_ref().is_some_and(|(m, _)| *m == uid)) {
|
||||
@@ -879,6 +900,10 @@ fn save_to(s: &mut Session, path: Option<String>) -> Result<Value> {
|
||||
}
|
||||
let i = crate::branches::file_tab(s);
|
||||
let ds = s.docs.get_mut(i).ok_or(EngineError::NoDocument)?;
|
||||
// A download (no path) writes over no file.
|
||||
if let Some(path) = path.as_deref() {
|
||||
refuse_dropping_images(ds, path)?;
|
||||
}
|
||||
let mut snapshot = FileSnapshot::of(ds);
|
||||
let follows_path = ds.path.as_deref().map_or(ds.doc.name == "Untitled", |old| ds.doc.name == file_stem(old));
|
||||
if let Some(path) = path.as_deref()
|
||||
|
||||
@@ -115,6 +115,11 @@ pub(crate) fn within_app_folder(path: &Path, folder: &Path) -> bool {
|
||||
matches!((std::fs::canonicalize(path), std::fs::canonicalize(folder)), (Ok(p), Ok(f)) if p.starts_with(&f))
|
||||
}
|
||||
|
||||
pub fn same_file(a: impl AsRef<Path>, b: impl AsRef<Path>) -> bool {
|
||||
let (a, b) = (a.as_ref(), b.as_ref());
|
||||
a == b || same_file::is_same_file(a, b).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// `p` without Windows' `\\?\` prefix, for messages.
|
||||
fn shown(p: &Path) -> String {
|
||||
let s = p.to_string_lossy();
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
use crate::cmd::file::FileSnapshot;
|
||||
|
||||
fn rect(s: &mut Session, x: f64) {
|
||||
s.execute("shape.rectangle", &json!({"x": x, "y": 0, "width": 10, "height": 10})).unwrap();
|
||||
}
|
||||
|
||||
fn opened_with_images_left_in_file(name: &str) -> (std::path::PathBuf, String, Session) {
|
||||
let dir = std::env::temp_dir().join(format!("figcraft-images-left-{name}-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("big.figcraft").to_string_lossy().to_string();
|
||||
let mut s = Session::new();
|
||||
rect(&mut s, 0.0);
|
||||
s.execute("file.save", &json!({"path": path})).unwrap();
|
||||
let mut t = Session::empty();
|
||||
t.execute("file.open", &json!({"path": path})).unwrap();
|
||||
let i = t.active;
|
||||
let tab = t.docs.remove(i).with_images_left_in_file(&path, 3);
|
||||
t.docs.insert(i, tab);
|
||||
(dir, path, t)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_with_images_left_out_is_not_saved_over() {
|
||||
let (dir, path, mut s) = opened_with_images_left_in_file("save");
|
||||
let before = std::fs::read(&path).unwrap();
|
||||
rect(&mut s, 20.0);
|
||||
let err = s.execute("file.save", &json!({})).unwrap_err().to_string();
|
||||
assert!(err.contains("3 of its images") && err.contains("another name"), "{err}");
|
||||
assert_eq!(std::fs::read(&path).unwrap(), before);
|
||||
assert!(s.ds().unwrap().is_dirty());
|
||||
|
||||
let copy = dir.join("copy.figcraft").to_string_lossy().to_string();
|
||||
s.execute("file.saveAs", &json!({"path": copy})).unwrap();
|
||||
assert!(!s.ds().unwrap().is_dirty());
|
||||
rect(&mut s, 40.0);
|
||||
s.execute("file.save", &json!({})).unwrap();
|
||||
assert!(s.execute("file.saveAs", &json!({"path": path})).is_err());
|
||||
assert_eq!(std::fs::read(&path).unwrap(), before);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_snapshot_encoded_in_the_background_does_not_save_over_it() {
|
||||
let (dir, path, mut s) = opened_with_images_left_in_file("snapshot");
|
||||
let before = std::fs::read(&path).unwrap();
|
||||
rect(&mut s, 20.0);
|
||||
let file = FileSnapshot::of(s.ds().unwrap());
|
||||
let bytes = file.encode().unwrap();
|
||||
assert!(file.save(&mut s, &bytes).is_err());
|
||||
assert_eq!(std::fs::read(&path).unwrap(), before);
|
||||
assert!(s.ds().unwrap().is_dirty());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_without_images_left_out_saves_as_before() {
|
||||
let (dir, path, mut s) = opened_with_images_left_in_file("none");
|
||||
let i = s.active;
|
||||
let tab = s.docs.remove(i).with_images_left_in_file(&path, 0);
|
||||
s.docs.insert(i, tab);
|
||||
rect(&mut s, 20.0);
|
||||
s.execute("file.save", &json!({})).unwrap();
|
||||
assert!(!s.ds().unwrap().is_dirty());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hard_link_to_the_file_is_not_saved_over() {
|
||||
let (dir, path, mut s) = opened_with_images_left_in_file("hard-link");
|
||||
let link = dir.join("link.figcraft");
|
||||
let _ = std::fs::remove_file(&link);
|
||||
std::fs::hard_link(&path, &link).unwrap();
|
||||
let before = std::fs::read(&path).unwrap();
|
||||
rect(&mut s, 20.0);
|
||||
assert!(s.execute("file.saveAs", &json!({"path": link.to_string_lossy()})).is_err());
|
||||
assert_eq!(std::fs::read(&path).unwrap(), before);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
@@ -372,6 +372,7 @@ pub struct DocState {
|
||||
/// The last duplicate while its copies are the selection: Duplicate repeats its offset (see
|
||||
/// [`cmd::edit::Duplicated`]; not saved).
|
||||
pub(crate) last_duplicate: Option<cmd::edit::Duplicated>,
|
||||
images_left_in_file: Option<(String, usize)>,
|
||||
}
|
||||
|
||||
/// A version shown on the canvas in place of the document: read-only until it's restored or the
|
||||
@@ -434,11 +435,21 @@ impl DocState {
|
||||
rotation_origin: None,
|
||||
view_from_prefs: false,
|
||||
last_duplicate: None,
|
||||
images_left_in_file: None,
|
||||
};
|
||||
st.zoom_to_fit();
|
||||
st
|
||||
}
|
||||
|
||||
pub fn with_images_left_in_file(mut self, path: &str, count: usize) -> Self {
|
||||
self.images_left_in_file = (count > 0).then(|| (path.to_string(), count));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn images_lost_saving_to(&self, path: &str) -> Option<usize> {
|
||||
self.images_left_in_file.as_ref().filter(|(file, _)| file_access::same_file(file, path)).map(|(_, count)| *count)
|
||||
}
|
||||
|
||||
/// The selected ruler guide (its owner and index): it counts while no layer is selected and it
|
||||
/// is still there on this page.
|
||||
pub fn selected_guide(&self) -> Option<(NodeId, usize)> {
|
||||
@@ -1727,6 +1738,8 @@ mod huge_stroke_tests;
|
||||
#[cfg(test)]
|
||||
mod id_params_behaviour_tests;
|
||||
#[cfg(test)]
|
||||
mod images_left_in_file_tests;
|
||||
#[cfg(test)]
|
||||
mod import_export_tests;
|
||||
#[cfg(test)]
|
||||
mod import_journal_tests;
|
||||
|
||||
Reference in New Issue
Block a user