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