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::{create_encoded, encode_json, read_record, write_encoded};
18use super::{
19 BackupLayoutGuard, JournalLock, JournalLockError, PersistenceError, commit_artifact_directory,
20};
21use crate::{
22 model::{
23 artifacts::canonical_hash,
24 download_journal::{
25 ArtifactStateRecord, ArtifactTransition, DownloadArtifactRequest,
26 DownloadJournalRecord, DownloadJournalRecordError, MAX_DOWNLOAD_JOURNAL_BYTES,
27 },
28 },
29 ops::artifacts::{ArtifactError, checksum_directory},
30};
31use std::{fs, io, path::PathBuf};
32use thiserror::Error;
33
34const JOURNAL_FILE: &str = "download-journal.json";
35
36#[derive(Debug)]
41pub struct DownloadJournalGuard<'a> {
42 layout: &'a BackupLayoutGuard,
43 _lock: JournalLock,
44 record: DownloadJournalRecord,
45 usable: bool,
46}
47
48impl<'a> DownloadJournalGuard<'a> {
49 pub fn create(
54 layout: &'a BackupLayoutGuard,
55 intent: &str,
56 artifacts: Vec<DownloadArtifactRequest>,
57 ) -> Result<Self, DownloadJournalError> {
58 layout.check_root()?;
59 let path = layout.root().join(JOURNAL_FILE);
60 let lock = JournalLock::acquire(&path)?;
61 let record = DownloadJournalRecord::new(intent, artifacts)?;
62 let bytes = encode_json(&record, MAX_DOWNLOAD_JOURNAL_BYTES)?;
63 create_encoded(&path, &bytes)?;
64 Ok(Self {
65 layout,
66 _lock: lock,
67 record,
68 usable: true,
69 })
70 }
71
72 pub fn open(
78 layout: &'a BackupLayoutGuard,
79 expected_intent: &str,
80 ) -> Result<Self, DownloadJournalError> {
81 layout.check_root()?;
82 let expected = canonical_hash(expected_intent).map_err(DownloadJournalRecordError::from)?;
83 let path = layout.root().join(JOURNAL_FILE);
84 let lock = JournalLock::acquire(&path)?;
85 let record: DownloadJournalRecord = read_record(&path, MAX_DOWNLOAD_JOURNAL_BYTES)?;
86 if record.intent() != expected {
87 return Err(DownloadJournalError::IntentMismatch);
88 }
89 Ok(Self {
90 layout,
91 _lock: lock,
92 record,
93 usable: true,
94 })
95 }
96
97 pub fn record(&self) -> Result<&DownloadJournalRecord, DownloadJournalError> {
102 self.check_usable()?;
103 Ok(&self.record)
104 }
105
106 #[must_use]
108 pub fn path(&self) -> PathBuf {
109 self.layout.root().join(JOURNAL_FILE)
110 }
111
112 pub fn record_downloaded(
120 &mut self,
121 canister: &str,
122 snapshot: &str,
123 ) -> Result<(), DownloadJournalError> {
124 let next = self.next(canister, snapshot, ArtifactTransition::Downloaded)?;
125 self.check_artifact_parent()?;
126 let entry = next.artifact(canister, snapshot)?;
127 checksum_directory(&self.layout.root().join(entry.staging_path()))?;
128 self.store(next, write_encoded)
129 }
130
131 pub fn verify_artifact(
136 &mut self,
137 canister: &str,
138 snapshot: &str,
139 ) -> Result<(), DownloadJournalError> {
140 self.check_usable()?;
141 let entry = self.record.artifact(canister, snapshot)?;
142 if entry.state() != ArtifactStateRecord::Downloaded {
143 return Err(DownloadJournalRecordError::InvalidStateTransition {
144 from: entry.state(),
145 to: ArtifactStateRecord::ChecksumVerified,
146 }
147 .into());
148 }
149 self.check_artifact_parent()?;
150 let checksum = checksum_directory(&self.layout.root().join(entry.staging_path()))?;
151 let next = self.next(
152 canister,
153 snapshot,
154 ArtifactTransition::ChecksumVerified(checksum),
155 )?;
156 self.store(next, write_encoded)
157 }
158
159 pub fn finalize_artifact(
166 &mut self,
167 canister: &str,
168 snapshot: &str,
169 ) -> Result<(), DownloadJournalError> {
170 self.finalize_with(canister, snapshot, write_encoded)
171 }
172
173 fn finalize_with(
174 &mut self,
175 canister: &str,
176 snapshot: &str,
177 write: impl FnOnce(&std::path::Path, &[u8]) -> Result<(), PersistenceError>,
178 ) -> Result<(), DownloadJournalError> {
179 let next = self.next(canister, snapshot, ArtifactTransition::Durable)?;
180 let bytes = encode_json(&next, MAX_DOWNLOAD_JOURNAL_BYTES)?;
181 self.check_artifact_parent()?;
182 let entry = next.artifact(canister, snapshot)?;
183 let checksum = entry
184 .checksum()
185 .ok_or(DownloadJournalRecordError::InvalidChecksumState(
186 ArtifactStateRecord::Durable,
187 ))?;
188 self.usable = false;
191 commit_artifact_directory(
192 &self.layout.root().join(entry.staging_path()),
193 &self.layout.root().join(entry.artifact_path()),
194 checksum.hash(),
195 )?;
196 self.store_encoded(next, &bytes, write)
197 }
198
199 fn next(
200 &self,
201 canister: &str,
202 snapshot: &str,
203 transition: ArtifactTransition,
204 ) -> Result<DownloadJournalRecord, DownloadJournalError> {
205 self.check_usable()?;
206 let mut next = self.record.clone();
207 next.advance(canister, snapshot, transition)?;
208 Ok(next)
209 }
210
211 fn check_usable(&self) -> Result<(), DownloadJournalError> {
212 if !self.usable {
213 return Err(DownloadJournalError::IndeterminateWrite);
214 }
215 self.layout.check_root()?;
216 Ok(())
217 }
218
219 fn check_artifact_parent(&self) -> Result<(), DownloadJournalError> {
220 let path = self.layout.root().join("artifacts");
221 if !fs::symlink_metadata(&path)?.is_dir() {
222 return Err(DownloadJournalError::UnsafeArtifactParent { path });
223 }
224 Ok(())
225 }
226
227 fn store(
228 &mut self,
229 next: DownloadJournalRecord,
230 write: impl FnOnce(&std::path::Path, &[u8]) -> Result<(), PersistenceError>,
231 ) -> Result<(), DownloadJournalError> {
232 let bytes = encode_json(&next, MAX_DOWNLOAD_JOURNAL_BYTES)?;
233 self.store_encoded(next, &bytes, write)
234 }
235
236 fn store_encoded(
237 &mut self,
238 next: DownloadJournalRecord,
239 bytes: &[u8],
240 write: impl FnOnce(&std::path::Path, &[u8]) -> Result<(), PersistenceError>,
241 ) -> Result<(), DownloadJournalError> {
242 self.layout.check_root()?;
243 let path = self.path();
244 super::json::replace_cached_record(&mut self.record, &mut self.usable, next, || {
245 write(&path, bytes)
246 })?;
247 Ok(())
248 }
249}
250
251#[derive(Debug, Error)]
253pub enum DownloadJournalError {
254 #[error("download journal immutable intent mismatch")]
256 IntentMismatch,
257 #[error("download journal outcome is indeterminate; reopen retained evidence")]
259 IndeterminateWrite,
260 #[error("unsafe download artifact parent: {path:?}")]
262 UnsafeArtifactParent {
263 path: PathBuf,
265 },
266 #[error(transparent)]
268 Record(#[from] DownloadJournalRecordError),
269 #[error(transparent)]
271 Lock(#[from] JournalLockError),
272 #[error(transparent)]
274 Persistence(#[from] PersistenceError),
275 #[error(transparent)]
277 Artifact(#[from] ArtifactError),
278 #[error(transparent)]
280 Io(#[from] io::Error),
281}
282
283#[cfg(all(test, unix))]
284mod tests;