canic_backup/persistence/
mod.rs1mod integrity;
2
3pub use integrity::{
4 ArtifactIntegrityReport, BackupExecutionIntegrityReport, BackupIntegrityReport,
5 resolve_backup_artifact_path,
6};
7use integrity::{verify_execution_integrity, verify_layout_integrity};
8
9#[cfg(test)]
10use crate::artifacts::ArtifactChecksum;
11use crate::{
12 artifacts::ArtifactChecksumError,
13 execution::{BackupExecutionJournal, BackupExecutionJournalError},
14 journal::DownloadJournal,
15 manifest::{FleetBackupManifest, ManifestValidationError},
16 plan::{BackupPlan, BackupPlanError},
17};
18use serde::Serialize;
19use serde::de::DeserializeOwned;
20use std::{
21 fs::{self, File},
22 io,
23 path::{Path, PathBuf},
24};
25use thiserror::Error as ThisError;
26
27const MANIFEST_FILE_NAME: &str = "fleet-backup-manifest.json";
28const BACKUP_PLAN_FILE_NAME: &str = "backup-plan.json";
29const JOURNAL_FILE_NAME: &str = "download-journal.json";
30const EXECUTION_JOURNAL_FILE_NAME: &str = "backup-execution-journal.json";
31
32#[derive(Clone, Debug)]
37pub struct BackupLayout {
38 root: PathBuf,
39}
40
41impl BackupLayout {
42 #[must_use]
44 pub const fn new(root: PathBuf) -> Self {
45 Self { root }
46 }
47
48 #[must_use]
50 pub fn root(&self) -> &Path {
51 &self.root
52 }
53
54 #[must_use]
56 pub fn manifest_path(&self) -> PathBuf {
57 self.root.join(MANIFEST_FILE_NAME)
58 }
59
60 #[must_use]
62 pub fn backup_plan_path(&self) -> PathBuf {
63 self.root.join(BACKUP_PLAN_FILE_NAME)
64 }
65
66 #[must_use]
68 pub fn journal_path(&self) -> PathBuf {
69 self.root.join(JOURNAL_FILE_NAME)
70 }
71
72 #[must_use]
74 pub fn execution_journal_path(&self) -> PathBuf {
75 self.root.join(EXECUTION_JOURNAL_FILE_NAME)
76 }
77
78 pub fn write_manifest(&self, manifest: &FleetBackupManifest) -> Result<(), PersistenceError> {
80 manifest.validate()?;
81 write_json_atomic(&self.manifest_path(), manifest)
82 }
83
84 pub fn read_manifest(&self) -> Result<FleetBackupManifest, PersistenceError> {
86 let manifest = read_json(&self.manifest_path())?;
87 FleetBackupManifest::validate(&manifest)?;
88 Ok(manifest)
89 }
90
91 pub fn write_backup_plan(&self, plan: &BackupPlan) -> Result<(), PersistenceError> {
93 plan.validate()?;
94 write_json_atomic(&self.backup_plan_path(), plan)
95 }
96
97 pub fn read_backup_plan(&self) -> Result<BackupPlan, PersistenceError> {
99 let plan = read_json(&self.backup_plan_path())?;
100 BackupPlan::validate(&plan)?;
101 Ok(plan)
102 }
103
104 pub fn write_journal(&self, journal: &DownloadJournal) -> Result<(), PersistenceError> {
106 journal.validate()?;
107 write_json_atomic(&self.journal_path(), journal)
108 }
109
110 pub fn read_journal(&self) -> Result<DownloadJournal, PersistenceError> {
112 let journal = read_json(&self.journal_path())?;
113 DownloadJournal::validate(&journal)?;
114 Ok(journal)
115 }
116
117 pub fn write_execution_journal(
119 &self,
120 journal: &BackupExecutionJournal,
121 ) -> Result<(), PersistenceError> {
122 journal.validate()?;
123 write_json_atomic(&self.execution_journal_path(), journal)
124 }
125
126 pub fn read_execution_journal(&self) -> Result<BackupExecutionJournal, PersistenceError> {
128 let journal = read_json(&self.execution_journal_path())?;
129 BackupExecutionJournal::validate(&journal)?;
130 Ok(journal)
131 }
132
133 pub fn verify_integrity(&self) -> Result<BackupIntegrityReport, PersistenceError> {
135 let manifest = self.read_manifest()?;
136 let journal = self.read_journal()?;
137 verify_layout_integrity(self, &manifest, &journal)
138 }
139
140 pub fn verify_execution_integrity(
142 &self,
143 ) -> Result<BackupExecutionIntegrityReport, PersistenceError> {
144 let plan = self.read_backup_plan()?;
145 let journal = self.read_execution_journal()?;
146 verify_execution_integrity(&plan, &journal)
147 }
148}
149
150#[derive(Debug, ThisError)]
155pub enum PersistenceError {
156 #[error(transparent)]
157 Io(#[from] io::Error),
158
159 #[error(transparent)]
160 Json(#[from] serde_json::Error),
161
162 #[error(transparent)]
163 InvalidManifest(#[from] ManifestValidationError),
164
165 #[error(transparent)]
166 InvalidJournal(#[from] crate::journal::JournalValidationError),
167
168 #[error(transparent)]
169 InvalidBackupPlan(#[from] BackupPlanError),
170
171 #[error(transparent)]
172 InvalidExecutionJournal(#[from] BackupExecutionJournalError),
173
174 #[error(transparent)]
175 Checksum(#[from] ArtifactChecksumError),
176
177 #[error("manifest backup id {manifest} does not match journal backup id {journal}")]
178 BackupIdMismatch { manifest: String, journal: String },
179
180 #[error("journal artifact {canister_id} snapshot {snapshot_id} is not durable")]
181 NonDurableArtifact {
182 canister_id: String,
183 snapshot_id: String,
184 },
185
186 #[error("journal artifact {canister_id} snapshot {snapshot_id} has no checksum")]
187 MissingJournalArtifactChecksum {
188 canister_id: String,
189 snapshot_id: String,
190 },
191
192 #[error("manifest member {canister_id} snapshot {snapshot_id} has no journal artifact")]
193 MissingJournalArtifact {
194 canister_id: String,
195 snapshot_id: String,
196 },
197
198 #[error("journal artifact {canister_id} snapshot {snapshot_id} is not declared in manifest")]
199 UnexpectedJournalArtifact {
200 canister_id: String,
201 snapshot_id: String,
202 },
203
204 #[error(
205 "manifest checksum for {canister_id} snapshot {snapshot_id} does not match journal checksum"
206 )]
207 ManifestJournalChecksumMismatch {
208 canister_id: String,
209 snapshot_id: String,
210 manifest: String,
211 journal: String,
212 },
213
214 #[error(
215 "manifest artifact path for {canister_id} snapshot {snapshot_id} does not match journal artifact path"
216 )]
217 ManifestJournalArtifactPathMismatch {
218 canister_id: String,
219 snapshot_id: String,
220 manifest: String,
221 journal: String,
222 },
223
224 #[error("manifest topology receipt {field} does not match journal topology receipt")]
225 ManifestJournalTopologyReceiptMismatch {
226 field: String,
227 manifest: String,
228 journal: Option<String>,
229 },
230
231 #[error("backup plan {field} does not match execution journal")]
232 PlanJournalMismatch {
233 field: &'static str,
234 plan: String,
235 journal: String,
236 },
237
238 #[error("backup plan operation {sequence} {field} does not match execution journal")]
239 PlanJournalOperationMismatch {
240 sequence: usize,
241 field: &'static str,
242 plan: String,
243 journal: String,
244 },
245
246 #[error("backup execution operation {sequence} is {state} but has no matching receipt")]
247 ExecutionOperationMissingReceipt { sequence: usize, state: String },
248
249 #[error("backup execution operation {sequence} timestamp does not match latest receipt")]
250 ExecutionOperationReceiptTimestampMismatch { sequence: usize },
251
252 #[error("artifact path escapes backup root: {artifact_path}")]
253 ArtifactPathEscapesBackup { artifact_path: String },
254
255 #[error("artifact path does not exist: {0}")]
256 MissingArtifact(String),
257}
258
259fn write_json_atomic<T>(path: &Path, value: &T) -> Result<(), PersistenceError>
261where
262 T: Serialize,
263{
264 if let Some(parent) = path.parent() {
265 fs::create_dir_all(parent)?;
266 }
267
268 let tmp_path = temp_path_for(path);
269 let mut file = File::create(&tmp_path)?;
270 serde_json::to_writer_pretty(&mut file, value)?;
271 file.sync_all()?;
272 drop(file);
273
274 fs::rename(&tmp_path, path)?;
275
276 if let Some(parent) = path.parent() {
277 File::open(parent)?.sync_all()?;
278 }
279
280 Ok(())
281}
282
283fn read_json<T>(path: &Path) -> Result<T, PersistenceError>
285where
286 T: DeserializeOwned,
287{
288 let file = File::open(path)?;
289 Ok(serde_json::from_reader(file)?)
290}
291
292fn temp_path_for(path: &Path) -> PathBuf {
294 let mut file_name = path
295 .file_name()
296 .and_then(|name| name.to_str())
297 .unwrap_or("canic-backup")
298 .to_string();
299 file_name.push_str(".tmp");
300 path.with_file_name(file_name)
301}
302
303#[cfg(test)]
304mod tests;