mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-14 14:12:44 +00:00
Autosave: a recovery listing judges each key once, so an autosave in progress isn't listed twice (#9324)
* Autosave: a recovery listing judges each key once, so an autosave in progress isn't listed twice * Autosave: the recovery listing builds for the browser too
This commit is contained in:
1 parent
c65b5d25d3
commit
b0622d20ca
1 file changed
+33
-22
@@ -9,6 +9,7 @@
|
||||
//! [`RecoveryStore`] is the app-facing lifecycle: an entry is only deleted once
|
||||
//! the document is saved or closed, never just because it was recovered.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -247,41 +248,51 @@ fn read_sidecar_bounded(path: &Path, max: usize) -> Option<Vec<u8>> {
|
||||
|
||||
/// Recoverable documents in `recovery_dir`, newest first. A sidecar must be named after its own
|
||||
/// (sanitized) key, so a stray or hand-edited one can never point outside the directory.
|
||||
///
|
||||
/// Each key is looked at once, after the directory has been read: an autosave may publish a
|
||||
/// document's first sidecar while the listing runs, and judging the bundle and the sidecar as
|
||||
/// each name came up listed that bundle twice, or not at all.
|
||||
pub fn list_recovery(recovery_dir: &Path) -> Vec<RecoveryEntry> {
|
||||
let mut out = Vec::new();
|
||||
let Ok(rd) = std::fs::read_dir(recovery_dir) else {
|
||||
return out;
|
||||
return Vec::new();
|
||||
};
|
||||
for e in rd.flatten() {
|
||||
let p = e.path();
|
||||
if e.file_type().is_ok_and(|ft| ft.is_file())
|
||||
&& p.extension().is_some_and(|x| x == "json")
|
||||
&& let Some(bytes) = read_sidecar_bounded(&p, 1 << 20)
|
||||
&& let Ok(info) = serde_json::from_slice::<RecoveryInfo>(&bytes)
|
||||
&& sanitize(&info.key) == info.key
|
||||
&& p.file_stem().is_some_and(|stem| *stem == *info.key)
|
||||
{
|
||||
let bundle = recovery_dir.join(format!("{}.pcraft", info.key));
|
||||
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);
|
||||
}
|
||||
}
|
||||
let keys: BTreeSet<String> = rd
|
||||
.flatten()
|
||||
.filter_map(|e| {
|
||||
let name = e.file_name().into_string().ok()?;
|
||||
name.strip_suffix(".json").or_else(|| name.strip_suffix(".pcraft")).map(str::to_string)
|
||||
})
|
||||
.collect();
|
||||
let mut out: Vec<RecoveryEntry> = keys.iter().filter_map(|key| recovery_entry(recovery_dir, key)).collect();
|
||||
out.sort_by(|a, b| b.info.saved_at.cmp(&a.info.saved_at).then(a.info.key.cmp(&b.info.key)));
|
||||
out
|
||||
}
|
||||
|
||||
/// `key`'s entry. The sidecar is tried again last: it can appear after the first look, once the
|
||||
/// bundle it describes is committed, and is never removed by a save.
|
||||
fn recovery_entry(recovery_dir: &Path, key: &str) -> Option<RecoveryEntry> {
|
||||
let bundle = recovery_dir.join(format!("{key}.pcraft"));
|
||||
sidecar_entry(recovery_dir, key, &bundle).or_else(|| sidecarless_entry(recovery_dir, &bundle)).or_else(|| sidecar_entry(recovery_dir, key, &bundle))
|
||||
}
|
||||
|
||||
fn sidecar_entry(recovery_dir: &Path, key: &str, bundle: &Path) -> Option<RecoveryEntry> {
|
||||
let bytes = read_sidecar_bounded(&recovery_dir.join(format!("{key}.json")), 1 << 20)?;
|
||||
let info = serde_json::from_slice::<RecoveryInfo>(&bytes).ok()?;
|
||||
(sanitize(&info.key) == info.key && info.key == key && bundle.join(crate::store::MANIFEST).is_file())
|
||||
.then(|| RecoveryEntry { info, bundle: bundle.to_path_buf() })
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
if !std::fs::symlink_metadata(bundle).is_ok_and(|m| m.is_dir())
|
||||
|| 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())?;
|
||||
|
||||
Reference in new issue
Block a user