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::{DeploymentBackupManifest, 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 = "deployment-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(
80 &self,
81 manifest: &DeploymentBackupManifest,
82 ) -> Result<(), PersistenceError> {
83 manifest.validate()?;
84 write_json_atomic(&self.manifest_path(), manifest)
85 }
86
87 pub fn read_manifest(&self) -> Result<DeploymentBackupManifest, PersistenceError> {
89 let manifest = read_json(&self.manifest_path())?;
90 DeploymentBackupManifest::validate(&manifest)?;
91 Ok(manifest)
92 }
93
94 pub fn write_backup_plan(&self, plan: &BackupPlan) -> Result<(), PersistenceError> {
96 plan.validate()?;
97 write_json_atomic(&self.backup_plan_path(), plan)
98 }
99
100 pub fn read_backup_plan(&self) -> Result<BackupPlan, PersistenceError> {
102 let plan = read_json(&self.backup_plan_path())?;
103 BackupPlan::validate(&plan)?;
104 Ok(plan)
105 }
106
107 pub fn write_journal(&self, journal: &DownloadJournal) -> Result<(), PersistenceError> {
109 journal.validate()?;
110 write_json_atomic(&self.journal_path(), journal)
111 }
112
113 pub fn read_journal(&self) -> Result<DownloadJournal, PersistenceError> {
115 let journal = read_json(&self.journal_path())?;
116 DownloadJournal::validate(&journal)?;
117 Ok(journal)
118 }
119
120 pub fn write_execution_journal(
122 &self,
123 journal: &BackupExecutionJournal,
124 ) -> Result<(), PersistenceError> {
125 journal.validate()?;
126 write_json_atomic(&self.execution_journal_path(), journal)
127 }
128
129 pub fn read_execution_journal(&self) -> Result<BackupExecutionJournal, PersistenceError> {
131 let journal = read_json(&self.execution_journal_path())?;
132 BackupExecutionJournal::validate(&journal)?;
133 Ok(journal)
134 }
135
136 pub fn verify_integrity(&self) -> Result<BackupIntegrityReport, PersistenceError> {
138 let manifest = self.read_manifest()?;
139 let journal = self.read_journal()?;
140 verify_layout_integrity(self, &manifest, &journal)
141 }
142
143 pub fn verify_execution_integrity(
145 &self,
146 ) -> Result<BackupExecutionIntegrityReport, PersistenceError> {
147 let plan = self.read_backup_plan()?;
148 let journal = self.read_execution_journal()?;
149 verify_execution_integrity(&plan, &journal)
150 }
151}
152
153#[derive(Debug, ThisError)]
158pub enum PersistenceError {
159 #[error(transparent)]
160 Io(#[from] io::Error),
161
162 #[error(transparent)]
163 Json(#[from] serde_json::Error),
164
165 #[error(transparent)]
166 InvalidManifest(#[from] ManifestValidationError),
167
168 #[error(transparent)]
169 InvalidJournal(#[from] crate::journal::JournalValidationError),
170
171 #[error(transparent)]
172 InvalidBackupPlan(#[from] BackupPlanError),
173
174 #[error(transparent)]
175 InvalidExecutionJournal(#[from] BackupExecutionJournalError),
176
177 #[error(transparent)]
178 Checksum(#[from] ArtifactChecksumError),
179
180 #[error("manifest backup id {manifest} does not match journal backup id {journal}")]
181 BackupIdMismatch { manifest: String, journal: String },
182
183 #[error("journal artifact {canister_id} snapshot {snapshot_id} is not durable")]
184 NonDurableArtifact {
185 canister_id: String,
186 snapshot_id: String,
187 },
188
189 #[error("journal artifact {canister_id} snapshot {snapshot_id} has no checksum")]
190 MissingJournalArtifactChecksum {
191 canister_id: String,
192 snapshot_id: String,
193 },
194
195 #[error("manifest member {canister_id} snapshot {snapshot_id} has no journal artifact")]
196 MissingJournalArtifact {
197 canister_id: String,
198 snapshot_id: String,
199 },
200
201 #[error("journal artifact {canister_id} snapshot {snapshot_id} is not declared in manifest")]
202 UnexpectedJournalArtifact {
203 canister_id: String,
204 snapshot_id: String,
205 },
206
207 #[error(
208 "manifest checksum for {canister_id} snapshot {snapshot_id} does not match journal checksum"
209 )]
210 ManifestJournalChecksumMismatch {
211 canister_id: String,
212 snapshot_id: String,
213 manifest: String,
214 journal: String,
215 },
216
217 #[error(
218 "manifest artifact path for {canister_id} snapshot {snapshot_id} does not match journal artifact path"
219 )]
220 ManifestJournalArtifactPathMismatch {
221 canister_id: String,
222 snapshot_id: String,
223 manifest: String,
224 journal: String,
225 },
226
227 #[error("manifest topology receipt {field} does not match journal topology receipt")]
228 ManifestJournalTopologyReceiptMismatch {
229 field: String,
230 manifest: String,
231 journal: Option<String>,
232 },
233
234 #[error("backup plan {field} does not match execution journal")]
235 PlanJournalMismatch {
236 field: &'static str,
237 plan: String,
238 journal: String,
239 },
240
241 #[error("backup plan operation {sequence} {field} does not match execution journal")]
242 PlanJournalOperationMismatch {
243 sequence: usize,
244 field: &'static str,
245 plan: String,
246 journal: String,
247 },
248
249 #[error("backup execution operation {sequence} is {state} but has no matching receipt")]
250 ExecutionOperationMissingReceipt { sequence: usize, state: String },
251
252 #[error("backup execution operation {sequence} timestamp does not match latest receipt")]
253 ExecutionOperationReceiptTimestampMismatch { sequence: usize },
254
255 #[error("artifact path escapes backup root: {artifact_path}")]
256 ArtifactPathEscapesBackup { artifact_path: String },
257
258 #[error("artifact path does not exist: {0}")]
259 MissingArtifact(String),
260}
261
262fn write_json_atomic<T>(path: &Path, value: &T) -> Result<(), PersistenceError>
264where
265 T: Serialize,
266{
267 if let Some(parent) = path.parent() {
268 fs::create_dir_all(parent)?;
269 }
270
271 let tmp_path = temp_path_for(path);
272 let mut file = File::create(&tmp_path)?;
273 serde_json::to_writer_pretty(&mut file, value)?;
274 file.sync_all()?;
275 drop(file);
276
277 fs::rename(&tmp_path, path)?;
278
279 if let Some(parent) = path.parent() {
280 File::open(parent)?.sync_all()?;
281 }
282
283 Ok(())
284}
285
286fn read_json<T>(path: &Path) -> Result<T, PersistenceError>
288where
289 T: DeserializeOwned,
290{
291 let file = File::open(path)?;
292 Ok(serde_json::from_reader(file)?)
293}
294
295fn temp_path_for(path: &Path) -> PathBuf {
297 let mut file_name = path
298 .file_name()
299 .and_then(|name| name.to_str())
300 .unwrap_or("canic-backup")
301 .to_string();
302 file_name.push_str(".tmp");
303 path.with_file_name(file_name)
304}
305
306#[cfg(test)]
307mod tests;