Skip to main content

pacenotes_installer\restore/
point.rs

1/// The packages a ledger held at one moment, keyed as the ledger keys them.
2pub type Snapshot = std::collections::BTreeMap<String, crate::ledger::LedgerEntry>;
3
4/// One restore point, named by the UTC stamp of the run that followed it.
5#[derive(Clone, Debug, PartialEq, Eq)]
6pub struct Point {
7    pub name: String,
8    /// What was installed before that run.
9    pub snapshot: Snapshot,
10}
11
12/// Every readable restore point in a game folder, newest first.
13///
14/// Sorted by name, not modification time, because the names are UTC stamps and
15/// a point copied back from a backup would otherwise sort as the newest run. A
16/// point whose snapshot cannot be read is skipped: it cannot be described, so
17/// it cannot be offered.
18pub fn points(game_dir: &std::path::Path) -> Vec<Point> {
19    let dir = game_dir
20        .join(crate::ledger::STATE_DIR)
21        .join(crate::engine::RESTORE_DIR);
22    let Ok(entries) = std::fs::read_dir(&dir) else {
23        return Vec::new();
24    };
25    let mut found: Vec<Point> = entries
26        .flatten()
27        .filter(|entry| entry.path().is_dir())
28        .filter_map(|entry| read(game_dir, &entry.file_name().to_string_lossy()))
29        .collect();
30    found.sort_by(|a, b| b.name.cmp(&a.name));
31    found
32}
33
34/// The point called `name`, or `None` when it has no readable snapshot.
35pub fn read(game_dir: &std::path::Path, name: &str) -> Option<Point> {
36    let path = crate::engine::snapshot_path(game_dir, name);
37    let snapshot = crate::ledger::Ledger::read_snapshot(&path)?;
38    Some(Point {
39        name: name.to_string(),
40        snapshot,
41    })
42}
43
44/// A point's name as a person reads a date, `2026-10-27 12:11`.
45///
46/// Seconds and the `-2` suffix of two runs in one second are dropped, since the
47/// user is choosing between runs, not timing them; a name of any other shape is
48/// shown as it is rather than as a blank row.
49pub fn human_time(point: &str) -> String {
50    let stem = point.split('Z').next().unwrap_or(point);
51    let Some((date, time)) = stem.split_once('T') else {
52        return point.to_string();
53    };
54    let mut parts = time.split('-');
55    match (parts.next(), parts.next()) {
56        (Some(hours), Some(minutes)) if date.len() == 10 => format!("{date} {hours}:{minutes}"),
57        _ => point.to_string(),
58    }
59}
60
61/// The payload digests this folder's ledger and every restore point name: what
62/// a sweep of the cache must keep beyond what its index names, since a restore
63/// rolls back to an older payload the index has moved past.
64///
65/// `None` when any of them cannot be read — a ledger or point written by a
66/// newer build, a damaged file — since what it needs is then unknown, and a
67/// wrong guess would delete someone's way back.
68pub fn payloads_in_use(game_dir: &std::path::Path) -> Option<Vec<String>> {
69    let ledger = crate::ledger::Ledger::open(game_dir);
70    if ledger.state() == crate::ledger::LedgerState::Unreadable {
71        return None;
72    }
73    let points = points(game_dir);
74    // `points` skips a point it cannot read, which here must stop the sweep.
75    let directories = std::fs::read_dir(
76        game_dir
77            .join(crate::ledger::STATE_DIR)
78            .join(crate::engine::RESTORE_DIR),
79    )
80    .map_or(0, |entries| {
81        entries
82            .flatten()
83            .filter(|entry| entry.path().is_dir())
84            .count()
85    });
86    if points.len() != directories {
87        return None;
88    }
89    let snapshots = points.iter().flat_map(|point| point.snapshot.values());
90    Some(
91        ledger
92            .packages()
93            .values()
94            .chain(snapshots)
95            .map(|entry| entry.sha256.clone())
96            .collect(),
97    )
98}