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:
aiongg authored and GitHub committed 2026-10-25 01:09:21 -04:00
1 parent c65b5d25d3
commit b0622d20ca
1 file changed
+33 -22
+33 -22
View File
@@ -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())?;