mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 19:26:51 -04:00
Compare commits
3
Commits
706638b284
...
c699e3cda6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c699e3cda6 | ||
|
|
7250e6e29a | ||
|
|
ed7df56a24 |
No files matched your search
Generated
+29
-29
@@ -4613,7 +4613,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"async-io",
|
||||
@@ -4657,7 +4657,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-affinity"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
@@ -4667,7 +4667,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-algo"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"photocraft-color",
|
||||
"photocraft-geom",
|
||||
@@ -4680,7 +4680,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-automation"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"cap-std",
|
||||
@@ -4702,7 +4702,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-cli"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"photocraft-automation",
|
||||
"photocraft-codecs",
|
||||
@@ -4716,7 +4716,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-cms"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"moxcms",
|
||||
"rayon",
|
||||
@@ -4724,7 +4724,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-codecs"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"exr",
|
||||
"flate2",
|
||||
@@ -4745,7 +4745,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-color"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"photocraft-cms",
|
||||
"serde",
|
||||
@@ -4753,7 +4753,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-compose"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"photocraft-color",
|
||||
@@ -4768,7 +4768,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-doc"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"photocraft-color",
|
||||
"photocraft-geom",
|
||||
@@ -4779,7 +4779,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-engine"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"craft-print",
|
||||
@@ -4814,7 +4814,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-format"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"crc32fast",
|
||||
@@ -4835,14 +4835,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-geom"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-gpu"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"half",
|
||||
"log",
|
||||
@@ -4859,14 +4859,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-heif"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"heic-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-io"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"crc32fast",
|
||||
@@ -4895,14 +4895,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-ops"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"photocraft-doc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-paint"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"photocraft-color",
|
||||
"photocraft-geom",
|
||||
@@ -4913,7 +4913,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-plugins"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"photocraft-color",
|
||||
"photocraft-geom",
|
||||
@@ -4928,7 +4928,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-psd"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"photocraft-psd",
|
||||
@@ -4939,7 +4939,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-raster"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"photocraft-color",
|
||||
"photocraft-geom",
|
||||
@@ -4949,7 +4949,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-raw"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"image",
|
||||
@@ -4964,7 +4964,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-tablet"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"block2 0.6.2",
|
||||
"objc2 0.6.4",
|
||||
@@ -4976,7 +4976,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-testkit"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"memory-stats",
|
||||
"photocraft-color",
|
||||
@@ -4987,7 +4987,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-text"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"parley",
|
||||
@@ -5001,7 +5001,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-ui-egui"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"craft-print",
|
||||
@@ -5042,7 +5042,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-vector"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"photocraft-color",
|
||||
"photocraft-doc",
|
||||
@@ -5053,7 +5053,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "photocraft-web"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -8651,7 +8651,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
default-members = ["crates/*", "apps/photocraft", "apps/photocraft-cli", "xtask"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.8.2"
|
||||
version = "0.9.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
|
||||
@@ -907,6 +907,7 @@ mod tests {
|
||||
let mut app = launch(&recovery);
|
||||
new_doc(&mut app, "#ff0000");
|
||||
let revision = app.session.active().unwrap().revision;
|
||||
assert_ne!(revision, 0, "a sidecar-less bundle lists as revision 0");
|
||||
autosave(&mut app, &ctx);
|
||||
|
||||
let mut failed = false;
|
||||
@@ -927,12 +928,16 @@ mod tests {
|
||||
std::fs::remove_file(&recovery).unwrap();
|
||||
std::fs::create_dir(&recovery).unwrap();
|
||||
autosave(&mut app, &ctx);
|
||||
// The worker commits the bundle's manifest before it publishes the sidecar, and
|
||||
// `list_recovery` lists a committed sidecar-less bundle (revision 0) on its own, so
|
||||
// the first non-empty listing can come from the middle of the write. Wait for the write
|
||||
// to finish: the entry then carries the sidecar's revision.
|
||||
let mut recovered = Vec::new();
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
|
||||
while std::time::Instant::now() < deadline {
|
||||
prefs_ui::tick(&mut app, &ctx);
|
||||
recovered = list_recovery(&recovery);
|
||||
if !recovered.is_empty() {
|
||||
if recovered.iter().any(|e| e.info.revision == revision) {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
|
||||
@@ -199,12 +199,33 @@ pub fn list_recovery(recovery_dir: &Path) -> Vec<RecoveryEntry> {
|
||||
if bundle.join(crate::store::MANIFEST).is_file() {
|
||||
out.push(RecoveryEntry { info, bundle });
|
||||
}
|
||||
} else if e.file_type().is_ok_and(|ft| ft.is_dir())
|
||||
&& let Some(entry) = sidecarless_entry(recovery_dir, &p)
|
||||
{
|
||||
out.push(entry);
|
||||
}
|
||||
}
|
||||
out.sort_by(|a, b| b.info.saved_at.cmp(&a.info.saved_at).then(a.info.key.cmp(&b.info.key)));
|
||||
out
|
||||
}
|
||||
|
||||
/// A committed `<key>.pcraft` bundle (its manifest is written last) whose sidecar was never
|
||||
/// published, e.g. a crash during a document's first autosave. The metadata is rebuilt from the
|
||||
/// manifest (read like a load reads it) and its modification time. Incomplete bundles (no
|
||||
/// readable manifest) and bundles with anything at the sidecar's path are not entries.
|
||||
fn sidecarless_entry(recovery_dir: &Path, bundle: &Path) -> Option<RecoveryEntry> {
|
||||
let key = bundle.file_name()?.to_str()?.strip_suffix(".pcraft")?;
|
||||
if key.is_empty() || sanitize(key) != key || std::fs::symlink_metadata(recovery_dir.join(format!("{key}.json"))).is_ok() {
|
||||
return None;
|
||||
}
|
||||
let meta = std::fs::symlink_metadata(bundle.join(crate::store::MANIFEST)).ok().filter(|m| m.is_file())?;
|
||||
let source = crate::store::DirSource { root: bundle.to_path_buf() };
|
||||
let manifest = crate::store::read_manifest(&source, &crate::LoadOptions::default()).ok()?;
|
||||
let saved_at = meta.modified().ok().and_then(|t| t.duration_since(UNIX_EPOCH).ok()).map_or(0, |d| d.as_secs());
|
||||
let info = RecoveryInfo { key: key.to_string(), document_name: manifest.document.name, original_path: None, saved_at, revision: 0 };
|
||||
Some(RecoveryEntry { info, bundle: bundle.to_path_buf() })
|
||||
}
|
||||
|
||||
/// Load a recovered document.
|
||||
pub fn recover(entry: &RecoveryEntry) -> Result<Document> {
|
||||
crate::load_path(&entry.bundle)
|
||||
@@ -292,8 +313,9 @@ impl RecoveryStore {
|
||||
const ORPHAN_AGE: std::time::Duration = std::time::Duration::from_secs(24 * 60 * 60);
|
||||
|
||||
/// Best-effort removal of recovery data nothing can ever load or clean up:
|
||||
/// - `<key>.pcraft` bundle folders with no `<key>.json` sidecar (a crash during a document's
|
||||
/// first autosave: the sidecar is written after the bundle, and `list_recovery` skips them);
|
||||
/// - `<key>.pcraft` bundle folders with no `<key>.json` sidecar and no readable manifest (a
|
||||
/// crash in the middle of a document's first autosave). A sidecar-less bundle whose manifest
|
||||
/// was committed is complete and listed by `list_recovery`, so it is kept;
|
||||
/// - `*.pcsave.tmp` files left by an interrupted atomic write (see [`crate::atomic::temp_name`])
|
||||
/// that another process created.
|
||||
///
|
||||
@@ -321,7 +343,8 @@ fn sweep_orphans(recovery_dir: &Path, min_age: std::time::Duration) {
|
||||
// Anything at the sidecar's path (even a broken one) keeps the bundle.
|
||||
let has_sidecar = std::fs::symlink_metadata(recovery_dir.join(format!("{key}.json"))).is_ok();
|
||||
let parts = [path.clone(), path.join("tiles"), path.join("blobs")];
|
||||
if !has_sidecar && parts.iter().all(|p| untouched_for(p, min_age, true)) {
|
||||
// A committed bundle is recoverable work: keep exactly what `list_recovery` lists.
|
||||
if !has_sidecar && sidecarless_entry(recovery_dir, &path).is_none() && parts.iter().all(|p| untouched_for(p, min_age, true)) {
|
||||
let _ = std::fs::remove_dir_all(&path);
|
||||
} else {
|
||||
bundles.extend(parts);
|
||||
@@ -437,4 +460,41 @@ mod tests {
|
||||
assert_eq!(recover(&entries[0]).unwrap(), *doc);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn a_committed_bundle_without_a_sidecar_is_listed_and_survives_the_sweep() {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"pcraft-nosidecar-{}-{}",
|
||||
std::process::id(),
|
||||
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0)
|
||||
));
|
||||
let doc =
|
||||
Arc::new(Document::new("Lost", photocraft_doc::Size { width: 4, height: 4 }, photocraft_color::ColorMode::Rgb, photocraft_color::SampleType::U8));
|
||||
let saver = Autosaver::new(&dir, "first");
|
||||
saver.request(doc.clone(), 1, None, SaveOptions::default());
|
||||
assert!(saver.flush().is_some_and(|r| r.is_ok()));
|
||||
// The crash: the bundle was committed, the sidecar never published.
|
||||
std::fs::remove_file(dir.join("first.json")).unwrap();
|
||||
|
||||
let entries = list_recovery(&dir);
|
||||
assert_eq!(entries.len(), 1, "the committed bundle is discoverable");
|
||||
assert_eq!((entries[0].info.key.as_str(), entries[0].info.document_name.as_str()), ("first", "Lost"));
|
||||
assert_eq!(recover(&entries[0]).unwrap(), *doc);
|
||||
|
||||
sweep_orphans(&dir, std::time::Duration::ZERO);
|
||||
assert!(dir.join("first.pcraft").is_dir(), "aged cleanup keeps a committed bundle");
|
||||
|
||||
// Incomplete bundles (no manifest, or one that does not read) are neither listed nor kept.
|
||||
let partial = dir.join("partial.pcraft");
|
||||
std::fs::create_dir_all(partial.join("tiles")).unwrap();
|
||||
let broken = dir.join("broken.pcraft");
|
||||
std::fs::create_dir_all(&broken).unwrap();
|
||||
std::fs::write(broken.join("manifest.json"), b"{").unwrap();
|
||||
assert_eq!(list_recovery(&dir).len(), 1);
|
||||
sweep_orphans(&dir, std::time::Duration::ZERO);
|
||||
assert!(!partial.exists() && !broken.exists());
|
||||
assert!(dir.join("first.pcraft").is_dir());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user