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