ic_backup/ops/persistence/download_journal/
mod.rs1#[cfg(unix)]
4mod ic_snapshot_artifact;
5mod integrity;
6mod local_restore_artifact;
7mod local_restore_source;
8mod manifest;
9#[cfg(unix)]
10mod metrics;
11#[cfg(unix)]
12pub use ic_snapshot_artifact::{
13 IcSnapshotArtifactError, IcSnapshotArtifactWriter, IcSnapshotUploadArtifactError,
14};
15pub use integrity::DownloadIntegrityError;
16pub use local_restore_artifact::{
17 LocalRestoreArtifactError, LocalRestoreArtifactPublicationError, LocalRestoreArtifactView,
18};
19pub use local_restore_source::LocalRestoreSourceError;
20pub use manifest::{DownloadManifestError, read_download_manifest};
21#[cfg(unix)]
22pub use metrics::{IcSnapshotLocalMetrics, MeasurementSummary};
23
24use super::{
25 BackupLayoutGuard, JournalLock, JournalLockError, PersistenceError, commit_artifact_directory,
26 create_json_durable, read_json, write_json_durable,
27};
28use crate::{
29 model::{
30 artifacts::{ArtifactChecksumRecord, canonical_hash},
31 download_journal::{
32 ArtifactStateRecord, DownloadArtifactRequest, DownloadJournalRecord,
33 DownloadJournalRecordError, MAX_DOWNLOAD_JOURNAL_BYTES,
34 },
35 },
36 ops::artifacts::{ArtifactError, checksum_directory},
37};
38use std::{fs, io, path::PathBuf};
39use thiserror::Error;
40
41const JOURNAL_FILE: &str = "download-journal.json";
42
43#[derive(Debug)]
48pub struct DownloadJournalGuard<'a> {
49 layout: &'a BackupLayoutGuard,
50 _lock: JournalLock,
51 record: DownloadJournalRecord,
52 usable: bool,
53 #[cfg(unix)]
54 ic_snapshot_metrics: std::sync::Mutex<IcSnapshotLocalMetrics>,
55}
56
57impl<'a> DownloadJournalGuard<'a> {
58 pub fn create(
63 layout: &'a BackupLayoutGuard,
64 intent: &str,
65 artifacts: Vec<DownloadArtifactRequest>,
66 ) -> Result<Self, DownloadJournalError> {
67 layout.check_root()?;
68 let path = layout.root().join(JOURNAL_FILE);
69 let lock = JournalLock::acquire(&path)?;
70 let record = DownloadJournalRecord::new(intent, artifacts)?;
71 check_size(&record)?;
72 create_json_durable(&path, &record)?;
73 Ok(Self {
74 layout,
75 _lock: lock,
76 record,
77 usable: true,
78 #[cfg(unix)]
79 ic_snapshot_metrics: std::sync::Mutex::default(),
80 })
81 }
82
83 pub fn open(
89 layout: &'a BackupLayoutGuard,
90 expected_intent: &str,
91 ) -> Result<Self, DownloadJournalError> {
92 layout.check_root()?;
93 let expected = canonical_hash(expected_intent).map_err(DownloadJournalRecordError::from)?;
94 let path = layout.root().join(JOURNAL_FILE);
95 let lock = JournalLock::acquire(&path)?;
96 let record: DownloadJournalRecord = read_json(&path, MAX_DOWNLOAD_JOURNAL_BYTES)?;
97 check_size(&record)?;
98 if record.intent() != expected {
99 return Err(DownloadJournalError::IntentMismatch);
100 }
101 Ok(Self {
102 layout,
103 _lock: lock,
104 record,
105 usable: true,
106 #[cfg(unix)]
107 ic_snapshot_metrics: std::sync::Mutex::default(),
108 })
109 }
110
111 pub fn record(&self) -> Result<&DownloadJournalRecord, DownloadJournalError> {
116 self.check_usable()?;
117 Ok(&self.record)
118 }
119
120 #[must_use]
122 pub fn path(&self) -> PathBuf {
123 self.layout.root().join(JOURNAL_FILE)
124 }
125
126 pub fn record_downloaded(
134 &mut self,
135 canister: &str,
136 snapshot: &str,
137 ) -> Result<(), DownloadJournalError> {
138 let next = self.next(canister, snapshot, ArtifactStateRecord::Downloaded, None)?;
139 self.check_artifact_parent()?;
140 let entry = next.artifact(canister, snapshot)?;
141 checksum_directory(&self.layout.root().join(entry.staging_path()))?;
142 self.store(next, write_json_durable)
143 }
144
145 pub fn verify_artifact(
150 &mut self,
151 canister: &str,
152 snapshot: &str,
153 ) -> Result<(), DownloadJournalError> {
154 self.check_usable()?;
155 let entry = self.record.artifact(canister, snapshot)?;
156 if entry.state() != ArtifactStateRecord::Downloaded {
157 return Err(DownloadJournalRecordError::InvalidStateTransition {
158 from: entry.state(),
159 to: ArtifactStateRecord::ChecksumVerified,
160 }
161 .into());
162 }
163 self.check_artifact_parent()?;
164 let checksum = checksum_directory(&self.layout.root().join(entry.staging_path()))?;
165 let next = self.next(
166 canister,
167 snapshot,
168 ArtifactStateRecord::ChecksumVerified,
169 Some(checksum),
170 )?;
171 self.store(next, write_json_durable)
172 }
173
174 pub fn finalize_artifact(
181 &mut self,
182 canister: &str,
183 snapshot: &str,
184 ) -> Result<(), DownloadJournalError> {
185 self.finalize_with(canister, snapshot, write_json_durable)
186 }
187
188 fn finalize_with(
189 &mut self,
190 canister: &str,
191 snapshot: &str,
192 write: impl FnOnce(&std::path::Path, &DownloadJournalRecord) -> Result<(), PersistenceError>,
193 ) -> Result<(), DownloadJournalError> {
194 let next = self.next(canister, snapshot, ArtifactStateRecord::Durable, None)?;
195 check_size(&next)?;
196 self.check_artifact_parent()?;
197 let entry = next.artifact(canister, snapshot)?;
198 let checksum = entry
199 .checksum()
200 .ok_or(DownloadJournalRecordError::InvalidChecksumState(
201 ArtifactStateRecord::Durable,
202 ))?;
203 self.usable = false;
206 commit_artifact_directory(
207 &self.layout.root().join(entry.staging_path()),
208 &self.layout.root().join(entry.artifact_path()),
209 checksum.hash(),
210 )?;
211 self.store(next, write)
212 }
213
214 fn next(
215 &self,
216 canister: &str,
217 snapshot: &str,
218 state: ArtifactStateRecord,
219 checksum: Option<ArtifactChecksumRecord>,
220 ) -> Result<DownloadJournalRecord, DownloadJournalError> {
221 self.check_usable()?;
222 let mut next = self.record.clone();
223 next.advance(canister, snapshot, state, checksum)?;
224 Ok(next)
225 }
226
227 fn check_usable(&self) -> Result<(), DownloadJournalError> {
228 if !self.usable {
229 return Err(DownloadJournalError::IndeterminateWrite);
230 }
231 self.layout.check_root()?;
232 Ok(())
233 }
234
235 fn check_artifact_parent(&self) -> Result<(), DownloadJournalError> {
236 let path = self.layout.root().join("artifacts");
237 if !fs::symlink_metadata(&path)?.is_dir() {
238 return Err(DownloadJournalError::UnsafeArtifactParent { path });
239 }
240 Ok(())
241 }
242
243 fn store(
244 &mut self,
245 next: DownloadJournalRecord,
246 write: impl FnOnce(&std::path::Path, &DownloadJournalRecord) -> Result<(), PersistenceError>,
247 ) -> Result<(), DownloadJournalError> {
248 check_size(&next)?;
249 self.layout.check_root()?;
250 self.usable = false;
251 write(&self.path(), &next)?;
252 self.record = next;
253 self.usable = true;
254 Ok(())
255 }
256}
257
258fn check_size(record: &DownloadJournalRecord) -> Result<(), PersistenceError> {
259 super::json::check_json_size(record, MAX_DOWNLOAD_JOURNAL_BYTES)
260}
261
262#[derive(Debug, Error)]
264pub enum DownloadJournalError {
265 #[error("download journal immutable intent mismatch")]
267 IntentMismatch,
268 #[error("download journal outcome is indeterminate; reopen retained evidence")]
270 IndeterminateWrite,
271 #[error("unsafe download artifact parent: {path:?}")]
273 UnsafeArtifactParent {
274 path: PathBuf,
276 },
277 #[error(transparent)]
279 Record(#[from] DownloadJournalRecordError),
280 #[error(transparent)]
282 Lock(#[from] JournalLockError),
283 #[error(transparent)]
285 Persistence(#[from] PersistenceError),
286 #[error(transparent)]
288 Artifact(#[from] ArtifactError),
289 #[error(transparent)]
291 Io(#[from] io::Error),
292}
293
294#[cfg(all(test, unix))]
295mod tests;