ic_backup/ops/persistence/download_journal/
mod.rs1#[cfg(unix)]
4mod ic_snapshot_artifact;
5mod integrity;
6mod local_restore_source;
7mod manifest;
8#[cfg(unix)]
9pub use ic_snapshot_artifact::{
10 IcSnapshotArtifactError, IcSnapshotArtifactWriter, IcSnapshotDataUploadPreparationError,
11 IcSnapshotUploadArtifactError,
12};
13pub use integrity::DownloadIntegrityError;
14pub use local_restore_source::LocalRestoreSourceError;
15pub use manifest::{DownloadManifestError, read_download_manifest};
16
17use super::json::{check_json_size, create_encoded, encode_json, write_encoded};
18use super::{
19 BackupLayoutGuard, JournalLock, JournalLockError, PersistenceError, commit_artifact_directory,
20 read_json,
21};
22use crate::{
23 model::{
24 artifacts::{ArtifactChecksumRecord, canonical_hash},
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 let bytes = encode_json(&record, MAX_DOWNLOAD_JOURNAL_BYTES)?;
64 create_encoded(&path, &bytes)?;
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 = canonical_hash(expected_intent).map_err(DownloadJournalRecordError::from)?;
84 let path = layout.root().join(JOURNAL_FILE);
85 let lock = JournalLock::acquire(&path)?;
86 let record: DownloadJournalRecord = read_json(&path, MAX_DOWNLOAD_JOURNAL_BYTES)?;
87 check_json_size(&record, MAX_DOWNLOAD_JOURNAL_BYTES)?;
88 if record.intent() != expected {
89 return Err(DownloadJournalError::IntentMismatch);
90 }
91 Ok(Self {
92 layout,
93 _lock: lock,
94 record,
95 usable: true,
96 })
97 }
98
99 pub fn record(&self) -> Result<&DownloadJournalRecord, DownloadJournalError> {
104 self.check_usable()?;
105 Ok(&self.record)
106 }
107
108 #[must_use]
110 pub fn path(&self) -> PathBuf {
111 self.layout.root().join(JOURNAL_FILE)
112 }
113
114 pub fn record_downloaded(
122 &mut self,
123 canister: &str,
124 snapshot: &str,
125 ) -> Result<(), DownloadJournalError> {
126 let next = self.next(canister, snapshot, ArtifactStateRecord::Downloaded, None)?;
127 self.check_artifact_parent()?;
128 let entry = next.artifact(canister, snapshot)?;
129 checksum_directory(&self.layout.root().join(entry.staging_path()))?;
130 self.store(next, write_encoded)
131 }
132
133 pub fn verify_artifact(
138 &mut self,
139 canister: &str,
140 snapshot: &str,
141 ) -> Result<(), DownloadJournalError> {
142 self.check_usable()?;
143 let entry = self.record.artifact(canister, snapshot)?;
144 if entry.state() != ArtifactStateRecord::Downloaded {
145 return Err(DownloadJournalRecordError::InvalidStateTransition {
146 from: entry.state(),
147 to: ArtifactStateRecord::ChecksumVerified,
148 }
149 .into());
150 }
151 self.check_artifact_parent()?;
152 let checksum = checksum_directory(&self.layout.root().join(entry.staging_path()))?;
153 let next = self.next(
154 canister,
155 snapshot,
156 ArtifactStateRecord::ChecksumVerified,
157 Some(checksum),
158 )?;
159 self.store(next, write_encoded)
160 }
161
162 pub fn finalize_artifact(
169 &mut self,
170 canister: &str,
171 snapshot: &str,
172 ) -> Result<(), DownloadJournalError> {
173 self.finalize_with(canister, snapshot, write_encoded)
174 }
175
176 fn finalize_with(
177 &mut self,
178 canister: &str,
179 snapshot: &str,
180 write: impl FnOnce(&std::path::Path, &[u8]) -> Result<(), PersistenceError>,
181 ) -> Result<(), DownloadJournalError> {
182 let next = self.next(canister, snapshot, ArtifactStateRecord::Durable, None)?;
183 let bytes = encode_json(&next, MAX_DOWNLOAD_JOURNAL_BYTES)?;
184 self.check_artifact_parent()?;
185 let entry = next.artifact(canister, snapshot)?;
186 let checksum = entry
187 .checksum()
188 .ok_or(DownloadJournalRecordError::InvalidChecksumState(
189 ArtifactStateRecord::Durable,
190 ))?;
191 self.usable = false;
194 commit_artifact_directory(
195 &self.layout.root().join(entry.staging_path()),
196 &self.layout.root().join(entry.artifact_path()),
197 checksum.hash(),
198 )?;
199 self.store_encoded(next, &bytes, write)
200 }
201
202 fn next(
203 &self,
204 canister: &str,
205 snapshot: &str,
206 state: ArtifactStateRecord,
207 checksum: Option<ArtifactChecksumRecord>,
208 ) -> Result<DownloadJournalRecord, DownloadJournalError> {
209 self.check_usable()?;
210 let mut next = self.record.clone();
211 next.advance(canister, snapshot, state, checksum)?;
212 Ok(next)
213 }
214
215 fn check_usable(&self) -> Result<(), DownloadJournalError> {
216 if !self.usable {
217 return Err(DownloadJournalError::IndeterminateWrite);
218 }
219 self.layout.check_root()?;
220 Ok(())
221 }
222
223 fn check_artifact_parent(&self) -> Result<(), DownloadJournalError> {
224 let path = self.layout.root().join("artifacts");
225 if !fs::symlink_metadata(&path)?.is_dir() {
226 return Err(DownloadJournalError::UnsafeArtifactParent { path });
227 }
228 Ok(())
229 }
230
231 fn store(
232 &mut self,
233 next: DownloadJournalRecord,
234 write: impl FnOnce(&std::path::Path, &[u8]) -> Result<(), PersistenceError>,
235 ) -> Result<(), DownloadJournalError> {
236 let bytes = encode_json(&next, MAX_DOWNLOAD_JOURNAL_BYTES)?;
237 self.store_encoded(next, &bytes, write)
238 }
239
240 fn store_encoded(
241 &mut self,
242 next: DownloadJournalRecord,
243 bytes: &[u8],
244 write: impl FnOnce(&std::path::Path, &[u8]) -> Result<(), PersistenceError>,
245 ) -> Result<(), DownloadJournalError> {
246 self.layout.check_root()?;
247 self.usable = false;
248 write(&self.path(), bytes)?;
249 self.record = next;
250 self.usable = true;
251 Ok(())
252 }
253}
254
255#[derive(Debug, Error)]
257pub enum DownloadJournalError {
258 #[error("download journal immutable intent mismatch")]
260 IntentMismatch,
261 #[error("download journal outcome is indeterminate; reopen retained evidence")]
263 IndeterminateWrite,
264 #[error("unsafe download artifact parent: {path:?}")]
266 UnsafeArtifactParent {
267 path: PathBuf,
269 },
270 #[error(transparent)]
272 Record(#[from] DownloadJournalRecordError),
273 #[error(transparent)]
275 Lock(#[from] JournalLockError),
276 #[error(transparent)]
278 Persistence(#[from] PersistenceError),
279 #[error(transparent)]
281 Artifact(#[from] ArtifactError),
282 #[error(transparent)]
284 Io(#[from] io::Error),
285}
286
287#[cfg(all(test, unix))]
288mod tests;