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::{
24 IcSnapshotLocalMetrics, MeasurementAggregate, MeasurementHistogram, MeasurementSummary,
25};
26
27use super::json::check_json_size;
28use super::{
29 BackupLayoutGuard, JournalLock, JournalLockError, PersistenceError, commit_artifact_directory,
30 create_json_durable, read_json, write_json_durable,
31};
32use crate::{
33 model::{
34 artifacts::{ArtifactChecksumRecord, canonical_hash},
35 download_journal::{
36 ArtifactStateRecord, DownloadArtifactRequest, DownloadJournalRecord,
37 DownloadJournalRecordError, MAX_DOWNLOAD_JOURNAL_BYTES,
38 },
39 },
40 ops::artifacts::{ArtifactError, checksum_directory},
41};
42use std::{fs, io, path::PathBuf};
43use thiserror::Error;
44
45const JOURNAL_FILE: &str = "download-journal.json";
46
47#[derive(Debug)]
52pub struct DownloadJournalGuard<'a> {
53 layout: &'a BackupLayoutGuard,
54 _lock: JournalLock,
55 record: DownloadJournalRecord,
56 usable: bool,
57 #[cfg(unix)]
58 ic_snapshot_metrics: std::sync::Mutex<IcSnapshotLocalMetrics>,
59}
60
61impl<'a> DownloadJournalGuard<'a> {
62 pub fn create(
67 layout: &'a BackupLayoutGuard,
68 intent: &str,
69 artifacts: Vec<DownloadArtifactRequest>,
70 ) -> Result<Self, DownloadJournalError> {
71 layout.check_root()?;
72 let path = layout.root().join(JOURNAL_FILE);
73 let lock = JournalLock::acquire(&path)?;
74 let record = DownloadJournalRecord::new(intent, artifacts)?;
75 check_json_size(&record, MAX_DOWNLOAD_JOURNAL_BYTES)?;
76 create_json_durable(&path, &record)?;
77 Ok(Self {
78 layout,
79 _lock: lock,
80 record,
81 usable: true,
82 #[cfg(unix)]
83 ic_snapshot_metrics: std::sync::Mutex::default(),
84 })
85 }
86
87 pub fn open(
93 layout: &'a BackupLayoutGuard,
94 expected_intent: &str,
95 ) -> Result<Self, DownloadJournalError> {
96 layout.check_root()?;
97 let expected = canonical_hash(expected_intent).map_err(DownloadJournalRecordError::from)?;
98 let path = layout.root().join(JOURNAL_FILE);
99 let lock = JournalLock::acquire(&path)?;
100 let record: DownloadJournalRecord = read_json(&path, MAX_DOWNLOAD_JOURNAL_BYTES)?;
101 check_json_size(&record, MAX_DOWNLOAD_JOURNAL_BYTES)?;
102 if record.intent() != expected {
103 return Err(DownloadJournalError::IntentMismatch);
104 }
105 Ok(Self {
106 layout,
107 _lock: lock,
108 record,
109 usable: true,
110 #[cfg(unix)]
111 ic_snapshot_metrics: std::sync::Mutex::default(),
112 })
113 }
114
115 pub fn record(&self) -> Result<&DownloadJournalRecord, DownloadJournalError> {
120 self.check_usable()?;
121 Ok(&self.record)
122 }
123
124 #[must_use]
126 pub fn path(&self) -> PathBuf {
127 self.layout.root().join(JOURNAL_FILE)
128 }
129
130 pub fn record_downloaded(
138 &mut self,
139 canister: &str,
140 snapshot: &str,
141 ) -> Result<(), DownloadJournalError> {
142 let next = self.next(canister, snapshot, ArtifactStateRecord::Downloaded, None)?;
143 self.check_artifact_parent()?;
144 let entry = next.artifact(canister, snapshot)?;
145 checksum_directory(&self.layout.root().join(entry.staging_path()))?;
146 self.store(next, write_json_durable)
147 }
148
149 pub fn verify_artifact(
154 &mut self,
155 canister: &str,
156 snapshot: &str,
157 ) -> Result<(), DownloadJournalError> {
158 self.check_usable()?;
159 let entry = self.record.artifact(canister, snapshot)?;
160 if entry.state() != ArtifactStateRecord::Downloaded {
161 return Err(DownloadJournalRecordError::InvalidStateTransition {
162 from: entry.state(),
163 to: ArtifactStateRecord::ChecksumVerified,
164 }
165 .into());
166 }
167 self.check_artifact_parent()?;
168 let checksum = checksum_directory(&self.layout.root().join(entry.staging_path()))?;
169 let next = self.next(
170 canister,
171 snapshot,
172 ArtifactStateRecord::ChecksumVerified,
173 Some(checksum),
174 )?;
175 self.store(next, write_json_durable)
176 }
177
178 pub fn finalize_artifact(
185 &mut self,
186 canister: &str,
187 snapshot: &str,
188 ) -> Result<(), DownloadJournalError> {
189 self.finalize_with(canister, snapshot, write_json_durable)
190 }
191
192 fn finalize_with(
193 &mut self,
194 canister: &str,
195 snapshot: &str,
196 write: impl FnOnce(&std::path::Path, &DownloadJournalRecord) -> Result<(), PersistenceError>,
197 ) -> Result<(), DownloadJournalError> {
198 let next = self.next(canister, snapshot, ArtifactStateRecord::Durable, None)?;
199 check_json_size(&next, MAX_DOWNLOAD_JOURNAL_BYTES)?;
200 self.check_artifact_parent()?;
201 let entry = next.artifact(canister, snapshot)?;
202 let checksum = entry
203 .checksum()
204 .ok_or(DownloadJournalRecordError::InvalidChecksumState(
205 ArtifactStateRecord::Durable,
206 ))?;
207 self.usable = false;
210 commit_artifact_directory(
211 &self.layout.root().join(entry.staging_path()),
212 &self.layout.root().join(entry.artifact_path()),
213 checksum.hash(),
214 )?;
215 self.store(next, write)
216 }
217
218 fn next(
219 &self,
220 canister: &str,
221 snapshot: &str,
222 state: ArtifactStateRecord,
223 checksum: Option<ArtifactChecksumRecord>,
224 ) -> Result<DownloadJournalRecord, DownloadJournalError> {
225 self.check_usable()?;
226 let mut next = self.record.clone();
227 next.advance(canister, snapshot, state, checksum)?;
228 Ok(next)
229 }
230
231 fn check_usable(&self) -> Result<(), DownloadJournalError> {
232 if !self.usable {
233 return Err(DownloadJournalError::IndeterminateWrite);
234 }
235 self.layout.check_root()?;
236 Ok(())
237 }
238
239 fn check_artifact_parent(&self) -> Result<(), DownloadJournalError> {
240 let path = self.layout.root().join("artifacts");
241 if !fs::symlink_metadata(&path)?.is_dir() {
242 return Err(DownloadJournalError::UnsafeArtifactParent { path });
243 }
244 Ok(())
245 }
246
247 fn store(
248 &mut self,
249 next: DownloadJournalRecord,
250 write: impl FnOnce(&std::path::Path, &DownloadJournalRecord) -> Result<(), PersistenceError>,
251 ) -> Result<(), DownloadJournalError> {
252 check_json_size(&next, MAX_DOWNLOAD_JOURNAL_BYTES)?;
253 self.layout.check_root()?;
254 self.usable = false;
255 write(&self.path(), &next)?;
256 self.record = next;
257 self.usable = true;
258 Ok(())
259 }
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;