#305: save a file from a snapshot encoded elsewhere

FileSnapshot takes what saving writes, encodes it (thumbnail and all) on any
thread, and writes it back as of the snapshot: edits since stay unsaved, and
bytes from before another save never overwrite the newer file.
This commit is contained in:
bflatastic
2026-10-15 18:11:52 +07:00
parent bebe509a25
commit 8616f3d1c5
2 changed files with 96 additions and 7 deletions
+30
View File
@@ -92,3 +92,33 @@ fn files_that_save_themselves_still_get_checkpoints() {
assert!(!autosave(&mut t));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_file_encoded_elsewhere_is_saved_as_of_its_snapshot() {
use crate::cmd::file::FileSnapshot;
let dir = std::env::temp_dir().join(format!("figcraft-snapshot-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("snap.figcraft").to_string_lossy().to_string();
let mut s = Session::new();
s.execute("file.save", &json!({"path": path})).unwrap();
rect(&mut s, 0.0);
let file = FileSnapshot::of(s.ds().unwrap());
let bytes = file.encode().unwrap();
rect(&mut s, 50.0);
assert!(file.save(&mut s, &bytes).unwrap());
assert!(s.ds().unwrap().is_dirty(), "the edit since the snapshot is still unsaved");
let mut t = Session::empty();
t.execute("file.open", &json!({"path": path})).unwrap();
assert_eq!(t.ds().unwrap().doc.page_content_bounds(t.ds().unwrap().page).unwrap().x1, 10.0, "the file is the snapshot");
// Saved meanwhile: the older bytes never overwrite the newer file.
let file = FileSnapshot::of(s.ds().unwrap());
let bytes = file.encode().unwrap();
rect(&mut s, 100.0);
s.execute("file.save", &json!({})).unwrap();
assert!(!file.save(&mut s, &bytes).unwrap());
assert!(!s.ds().unwrap().is_dirty());
let mut t = Session::empty();
t.execute("file.open", &json!({"path": path})).unwrap();
assert_eq!(t.ds().unwrap().doc.page_content_bounds(t.ds().unwrap().page).unwrap().x1, 110.0);
let _ = std::fs::remove_dir_all(&dir);
}
+66 -7
View File
@@ -696,10 +696,71 @@ pub fn thumbnail(doc: &Document) -> Option<Vec<u8>> {
/// The `.figcraft` file for a document: content, thumbnail and version history.
pub fn file_bytes(ds: &DocState) -> Result<Vec<u8>> {
let mut doc = (*ds.doc).clone();
doc.current_page = Some(ds.page);
let thumb = thumbnail(&doc);
figcraft_format::save_bytes_with_history(&doc, thumb.as_deref(), &ds.versions, &ds.branches).map_err(|e| EngineError::Other(e.to_string()))
FileSnapshot::of(ds).encode()
}
/// A document's file as it is at one moment, so the slow part of saving it (the thumbnail and the
/// encoding) can run on another thread while editing goes on: [`encode`](Self::encode) there, then
/// [`save`](Self::save) the bytes back on the session's thread.
pub struct FileSnapshot {
uid: u64,
path: Option<String>,
revision: u64,
doc: Arc<Document>,
/// The document as last saved when the snapshot was taken: another save since makes it stale.
saved: Arc<Document>,
page: NodeId,
versions: Vec<Version>,
branches: Vec<figcraft_format::Branch>,
}
impl FileSnapshot {
pub fn of(ds: &DocState) -> Self {
Self {
uid: ds.uid,
path: ds.path.clone(),
revision: ds.revision,
doc: ds.doc.clone(),
saved: ds.saved_doc.clone(),
page: ds.page,
versions: ds.versions.clone(),
branches: ds.branches.clone(),
}
}
/// The file's bytes: content, thumbnail and version history.
pub fn encode(&self) -> Result<Vec<u8>> {
let mut doc = (*self.doc).clone();
doc.current_page = Some(self.page);
let thumb = thumbnail(&doc);
figcraft_format::save_bytes_with_history(&doc, thumb.as_deref(), &self.versions, &self.branches)
.map_err(|e| EngineError::Other(e.to_string()))
}
/// Write `bytes` (this snapshot's [`encode`](Self::encode)) to the document's file: it is saved
/// as of the snapshot, and edits since stay unsaved. False (nothing written) when the document
/// was closed, saved, reverted or given another path since: the bytes are out of date.
pub fn save(self, s: &mut Session, bytes: &[u8]) -> Result<bool> {
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)?;
// The history or branches may have changed since: then they still count as unsaved.
if ds.revision == self.revision {
ds.versions_changed = false;
}
ds.saved_doc = self.doc;
ds.revision += 1;
branch_tabs_saved(s, self.uid);
Ok(true)
}
}
/// 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)) {
tab.mark_saved();
}
}
fn color_profile(s: &mut Session, p: &Value) -> Result<Value> {
@@ -724,9 +785,7 @@ fn save_to(s: &mut Session, path: String) -> Result<Value> {
ds.mark_saved();
ds.revision += 1;
let uid = ds.uid;
for tab in s.docs.iter_mut().filter(|d| d.branch_of.as_ref().is_some_and(|(m, _)| *m == uid)) {
tab.mark_saved();
}
branch_tabs_saved(s, uid);
remember(s, &path);
Ok(json!({"path": path, "bytes": bytes.len()}))
}