Skip to main content

powdb_backup/
manifest.rs

1use serde::{Deserialize, Serialize};
2use std::io;
3use std::path::Path;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct FileEntry {
7    pub name: String,
8    pub len: u64,
9    pub blake3_hex: String,
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct BackupManifest {
14    pub format_version: u32,
15    pub created_unix_secs: u64,
16    /// The page-LSN high-water mark this backup is consistent at.
17    pub source_lsn: u64,
18    pub files: Vec<FileEntry>,
19}
20
21impl BackupManifest {
22    pub const FORMAT_VERSION: u32 = 1;
23    pub const FILE_NAME: &'static str = "manifest.json";
24
25    pub fn validate_version(&self) -> io::Result<()> {
26        if self.format_version != Self::FORMAT_VERSION {
27            return Err(io::Error::other(format!(
28                "unsupported backup format {} (this build understands {})",
29                self.format_version,
30                Self::FORMAT_VERSION
31            )));
32        }
33        Ok(())
34    }
35
36    pub fn write(&self, dir: &Path) -> io::Result<()> {
37        let json = serde_json::to_vec_pretty(self).map_err(io::Error::other)?;
38        std::fs::write(dir.join(Self::FILE_NAME), json)
39    }
40
41    pub fn read(dir: &Path) -> io::Result<Self> {
42        let bytes = std::fs::read(dir.join(Self::FILE_NAME))?;
43        let m: BackupManifest = serde_json::from_slice(&bytes).map_err(io::Error::other)?;
44        m.validate_version()?;
45        Ok(m)
46    }
47}
48
49/// A file that changed (relative to a base) in an incremental backup.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub enum ChangedFile {
52    /// A small/unpaged file copied whole (e.g. catalog.bin).
53    Whole {
54        name: String,
55        len: u64,
56        blake3_hex: String,
57    },
58    /// A paged file (.heap/.idx): only pages whose LSN > base.source_lsn.
59    /// The sidecar delta file `<name>.delta` holds, for each listed page index
60    /// in order, a 4-byte LE page index followed by PAGE_SIZE bytes.
61    Pages {
62        name: String,
63        /// page count of the file at increment time
64        total_pages: u32,
65        /// which pages are in the delta (ascending)
66        page_indices: Vec<u32>,
67        /// "<name>.delta"
68        delta_file: String,
69        delta_len: u64,
70        delta_blake3_hex: String,
71    },
72}
73
74/// Manifest for an incremental (page-LSN diff) backup.
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct IncrementManifest {
77    /// reuse FORMAT_VERSION = 1
78    pub format_version: u32,
79    pub created_unix_secs: u64,
80    /// the source_lsn of the base (full or prior increment) this builds on
81    pub base_source_lsn: u64,
82    /// high-water mark after this increment (== catalog.max_lsn())
83    pub source_lsn: u64,
84    pub changed: Vec<ChangedFile>,
85}
86
87impl IncrementManifest {
88    pub const FORMAT_VERSION: u32 = 1;
89    pub const FILE_NAME: &'static str = "increment.json";
90
91    pub fn validate_version(&self) -> io::Result<()> {
92        if self.format_version != Self::FORMAT_VERSION {
93            return Err(io::Error::other(format!(
94                "unsupported increment format {} (this build understands {})",
95                self.format_version,
96                Self::FORMAT_VERSION
97            )));
98        }
99        Ok(())
100    }
101
102    pub fn write(&self, dir: &Path) -> io::Result<()> {
103        let json = serde_json::to_vec_pretty(self).map_err(io::Error::other)?;
104        std::fs::write(dir.join(Self::FILE_NAME), json)
105    }
106
107    pub fn read(dir: &Path) -> io::Result<Self> {
108        let bytes = std::fs::read(dir.join(Self::FILE_NAME))?;
109        let m: IncrementManifest = serde_json::from_slice(&bytes).map_err(io::Error::other)?;
110        m.validate_version()?;
111        Ok(m)
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    #[test]
119    fn manifest_round_trips_and_rejects_bad_version() {
120        let m = BackupManifest {
121            format_version: BackupManifest::FORMAT_VERSION,
122            created_unix_secs: 1_700_000_000,
123            source_lsn: 42,
124            files: vec![FileEntry {
125                name: "catalog.bin".into(),
126                len: 10,
127                blake3_hex: "ab".into(),
128            }],
129        };
130        let json = serde_json::to_string(&m).unwrap();
131        let back: BackupManifest = serde_json::from_str(&json).unwrap();
132        assert_eq!(back.source_lsn, 42);
133        assert_eq!(back.files.len(), 1);
134
135        let mut bad = m.clone();
136        bad.format_version = 999;
137        assert!(
138            bad.validate_version().is_err(),
139            "unknown format must be rejected"
140        );
141    }
142}