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)]
10pub use ic_snapshot_artifact::{IcSnapshotArtifactError, IcSnapshotArtifactWriter};
11pub use integrity::DownloadIntegrityError;
12pub use local_restore_artifact::{
13 LocalRestoreArtifactError, LocalRestoreArtifactPublicationError, LocalRestoreArtifactView,
14};
15pub use local_restore_source::LocalRestoreSourceError;
16pub use manifest::{DownloadManifestError, read_download_manifest};
17
18use super::{
19 BackupLayoutGuard, JournalLock, JournalLockError, PersistenceError, commit_artifact_directory,
20 create_json_durable, read_json, write_json_durable,
21};
22use crate::{
23 model::{
24 artifacts::ArtifactChecksumRecord,
25 download_journal::{
26 ArtifactStateRecord, DownloadArtifactRequest, DownloadJournalRecord,
27 DownloadJournalRecordError, MAX_DOWNLOAD_JOURNAL_BYTES,
28 },
29 },
30 ops::artifacts::{ArtifactError, checksum_directory},
31};
32use std::{fs, io, path::PathBuf};
33use thiserror::Error;
34
35const JOURNAL_FILE: &str = "download-journal.json";
36
37#[derive(Debug)]
42pub struct DownloadJournalGuard<'a> {
43 layout: &'a BackupLayoutGuard,
44 _lock: JournalLock,
45 record: DownloadJournalRecord,
46 usable: bool,
47}
48
49impl<'a> DownloadJournalGuard<'a> {
50 pub fn create(
55 layout: &'a BackupLayoutGuard,
56 intent: &str,
57 artifacts: Vec<DownloadArtifactRequest>,
58 ) -> Result<Self, DownloadJournalError> {
59 layout.check_root()?;
60 let path = layout.root().join(JOURNAL_FILE);
61 let lock = JournalLock::acquire(&path)?;
62 let record = DownloadJournalRecord::new(intent, artifacts)?;
63 check_size(&record)?;
64 create_json_durable(&path, &record)?;
65 Ok(Self {
66 layout,
67 _lock: lock,
68 record,
69 usable: true,
70 })
71 }
72
73 pub fn open(
79 layout: &'a BackupLayoutGuard,
80 expected_intent: &str,
81 ) -> Result<Self, DownloadJournalError> {
82 layout.check_root()?;
83 let expected = ArtifactChecksumRecord::from_hash(expected_intent)
84 .map_err(DownloadJournalRecordError::from)?;
85 let path = layout.root().join(JOURNAL_FILE);
86 let lock = JournalLock::acquire(&path)?;
87 let record: DownloadJournalRecord = read_json(&path, MAX_DOWNLOAD_JOURNAL_BYTES)?;
88 check_size(&record)?;
89 if record.intent() != expected.hash() {
90 return Err(DownloadJournalError::IntentMismatch);
91 }
92 Ok(Self {
93 layout,
94 _lock: lock,
95 record,
96 usable: true,
97 })
98 }
99
100 pub fn record(&self) -> Result<&DownloadJournalRecord, DownloadJournalError> {
105 self.check_usable()?;
106 Ok(&self.record)
107 }
108
109 #[must_use]
111 pub fn path(&self) -> PathBuf {
112 self.layout.root().join(JOURNAL_FILE)
113 }
114
115 pub fn record_downloaded(
123 &mut self,
124 canister: &str,
125 snapshot: &str,
126 ) -> Result<(), DownloadJournalError> {
127 let next = self.next(canister, snapshot, ArtifactStateRecord::Downloaded, None)?;
128 self.check_artifact_parent()?;
129 let entry = next.artifact(canister, snapshot)?;
130 checksum_directory(&self.layout.root().join(entry.staging_path()))?;
131 self.store(next, write_json_durable)
132 }
133
134 pub fn verify_artifact(
139 &mut self,
140 canister: &str,
141 snapshot: &str,
142 ) -> Result<(), DownloadJournalError> {
143 self.check_usable()?;
144 let entry = self.record.artifact(canister, snapshot)?;
145 if entry.state() != ArtifactStateRecord::Downloaded {
146 return Err(DownloadJournalRecordError::InvalidStateTransition {
147 from: entry.state(),
148 to: ArtifactStateRecord::ChecksumVerified,
149 }
150 .into());
151 }
152 self.check_artifact_parent()?;
153 let checksum = checksum_directory(&self.layout.root().join(entry.staging_path()))?;
154 let next = self.next(
155 canister,
156 snapshot,
157 ArtifactStateRecord::ChecksumVerified,
158 Some(checksum),
159 )?;
160 self.store(next, write_json_durable)
161 }
162
163 pub fn finalize_artifact(
170 &mut self,
171 canister: &str,
172 snapshot: &str,
173 ) -> Result<(), DownloadJournalError> {
174 self.finalize_with(canister, snapshot, write_json_durable)
175 }
176
177 fn finalize_with(
178 &mut self,
179 canister: &str,
180 snapshot: &str,
181 write: impl FnOnce(&std::path::Path, &DownloadJournalRecord) -> Result<(), PersistenceError>,
182 ) -> Result<(), DownloadJournalError> {
183 let next = self.next(canister, snapshot, ArtifactStateRecord::Durable, None)?;
184 check_size(&next)?;
185 self.check_artifact_parent()?;
186 let entry = next.artifact(canister, snapshot)?;
187 let checksum = entry
188 .checksum()
189 .ok_or(DownloadJournalRecordError::InvalidChecksumState(
190 ArtifactStateRecord::Durable,
191 ))?;
192 self.usable = false;
195 commit_artifact_directory(
196 &self.layout.root().join(entry.staging_path()),
197 &self.layout.root().join(entry.artifact_path()),
198 checksum.hash(),
199 )?;
200 self.store(next, write)
201 }
202
203 fn next(
204 &self,
205 canister: &str,
206 snapshot: &str,
207 state: ArtifactStateRecord,
208 checksum: Option<ArtifactChecksumRecord>,
209 ) -> Result<DownloadJournalRecord, DownloadJournalError> {
210 self.check_usable()?;
211 let mut next = self.record.clone();
212 next.advance(canister, snapshot, state, checksum)?;
213 Ok(next)
214 }
215
216 fn check_usable(&self) -> Result<(), DownloadJournalError> {
217 if !self.usable {
218 return Err(DownloadJournalError::IndeterminateWrite);
219 }
220 self.layout.check_root()?;
221 Ok(())
222 }
223
224 fn check_artifact_parent(&self) -> Result<(), DownloadJournalError> {
225 let path = self.layout.root().join("artifacts");
226 if !fs::symlink_metadata(&path)?.is_dir() {
227 return Err(DownloadJournalError::UnsafeArtifactParent { path });
228 }
229 Ok(())
230 }
231
232 fn store(
233 &mut self,
234 next: DownloadJournalRecord,
235 write: impl FnOnce(&std::path::Path, &DownloadJournalRecord) -> Result<(), PersistenceError>,
236 ) -> Result<(), DownloadJournalError> {
237 check_size(&next)?;
238 self.layout.check_root()?;
239 self.usable = false;
240 write(&self.path(), &next)?;
241 self.record = next;
242 self.usable = true;
243 Ok(())
244 }
245}
246
247fn check_size(record: &DownloadJournalRecord) -> Result<(), PersistenceError> {
248 if serde_json::to_vec_pretty(record)?.len() as u64 > MAX_DOWNLOAD_JOURNAL_BYTES {
249 return Err(PersistenceError::RecordTooLarge {
250 limit: MAX_DOWNLOAD_JOURNAL_BYTES,
251 });
252 }
253 Ok(())
254}
255
256#[derive(Debug, Error)]
258pub enum DownloadJournalError {
259 #[error("download journal immutable intent mismatch")]
261 IntentMismatch,
262 #[error("download journal outcome is indeterminate; reopen retained evidence")]
264 IndeterminateWrite,
265 #[error("unsafe download artifact parent: {path:?}")]
267 UnsafeArtifactParent {
268 path: PathBuf,
270 },
271 #[error(transparent)]
273 Record(#[from] DownloadJournalRecordError),
274 #[error(transparent)]
276 Lock(#[from] JournalLockError),
277 #[error(transparent)]
279 Persistence(#[from] PersistenceError),
280 #[error(transparent)]
282 Artifact(#[from] ArtifactError),
283 #[error(transparent)]
285 Io(#[from] io::Error),
286}
287
288#[cfg(all(test, unix))]
289mod tests;