ic_backup/ops/persistence/download_journal/
mod.rs1mod integrity;
4mod local_restore_artifact;
5mod local_restore_source;
6mod manifest;
7pub use integrity::DownloadIntegrityError;
8pub use local_restore_artifact::{
9 LocalRestoreArtifactError, LocalRestoreArtifactPublicationError, LocalRestoreArtifactView,
10};
11pub use local_restore_source::LocalRestoreSourceError;
12pub use manifest::{DownloadManifestError, read_download_manifest};
13
14use super::{
15 BackupLayoutGuard, JournalLock, JournalLockError, PersistenceError, commit_artifact_directory,
16 create_json_durable, read_json, write_json_durable,
17};
18use crate::{
19 model::{
20 artifacts::ArtifactChecksumRecord,
21 download_journal::{
22 ArtifactStateRecord, DownloadArtifactRequest, DownloadJournalRecord,
23 DownloadJournalRecordError, MAX_DOWNLOAD_JOURNAL_BYTES,
24 },
25 },
26 ops::artifacts::{ArtifactError, checksum_directory},
27};
28use std::{fs, io, path::PathBuf};
29use thiserror::Error;
30
31const JOURNAL_FILE: &str = "download-journal.json";
32
33#[derive(Debug)]
38pub struct DownloadJournalGuard<'a> {
39 layout: &'a BackupLayoutGuard,
40 _lock: JournalLock,
41 record: DownloadJournalRecord,
42 usable: bool,
43}
44
45impl<'a> DownloadJournalGuard<'a> {
46 pub fn create(
51 layout: &'a BackupLayoutGuard,
52 intent: &str,
53 artifacts: Vec<DownloadArtifactRequest>,
54 ) -> Result<Self, DownloadJournalError> {
55 layout.check_root()?;
56 let path = layout.root().join(JOURNAL_FILE);
57 let lock = JournalLock::acquire(&path)?;
58 let record = DownloadJournalRecord::new(intent, artifacts)?;
59 check_size(&record)?;
60 create_json_durable(&path, &record)?;
61 Ok(Self {
62 layout,
63 _lock: lock,
64 record,
65 usable: true,
66 })
67 }
68
69 pub fn open(
75 layout: &'a BackupLayoutGuard,
76 expected_intent: &str,
77 ) -> Result<Self, DownloadJournalError> {
78 layout.check_root()?;
79 let expected = ArtifactChecksumRecord::from_hash(expected_intent)
80 .map_err(DownloadJournalRecordError::from)?;
81 let path = layout.root().join(JOURNAL_FILE);
82 let lock = JournalLock::acquire(&path)?;
83 let record: DownloadJournalRecord = read_json(&path, MAX_DOWNLOAD_JOURNAL_BYTES)?;
84 check_size(&record)?;
85 if record.intent() != expected.hash() {
86 return Err(DownloadJournalError::IntentMismatch);
87 }
88 Ok(Self {
89 layout,
90 _lock: lock,
91 record,
92 usable: true,
93 })
94 }
95
96 pub fn record(&self) -> Result<&DownloadJournalRecord, DownloadJournalError> {
101 self.check_usable()?;
102 Ok(&self.record)
103 }
104
105 #[must_use]
107 pub fn path(&self) -> PathBuf {
108 self.layout.root().join(JOURNAL_FILE)
109 }
110
111 pub fn record_downloaded(
119 &mut self,
120 canister: &str,
121 snapshot: &str,
122 ) -> Result<(), DownloadJournalError> {
123 let next = self.next(canister, snapshot, ArtifactStateRecord::Downloaded, None)?;
124 self.check_artifact_parent()?;
125 let entry = next.artifact(canister, snapshot)?;
126 checksum_directory(&self.layout.root().join(entry.staging_path()))?;
127 self.store(next, write_json_durable)
128 }
129
130 pub fn verify_artifact(
135 &mut self,
136 canister: &str,
137 snapshot: &str,
138 ) -> Result<(), DownloadJournalError> {
139 self.check_usable()?;
140 let entry = self.record.artifact(canister, snapshot)?;
141 if entry.state() != ArtifactStateRecord::Downloaded {
142 return Err(DownloadJournalRecordError::InvalidStateTransition {
143 from: entry.state(),
144 to: ArtifactStateRecord::ChecksumVerified,
145 }
146 .into());
147 }
148 self.check_artifact_parent()?;
149 let checksum = checksum_directory(&self.layout.root().join(entry.staging_path()))?;
150 let next = self.next(
151 canister,
152 snapshot,
153 ArtifactStateRecord::ChecksumVerified,
154 Some(checksum),
155 )?;
156 self.store(next, write_json_durable)
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_json_durable)
171 }
172
173 fn finalize_with(
174 &mut self,
175 canister: &str,
176 snapshot: &str,
177 write: impl FnOnce(&std::path::Path, &DownloadJournalRecord) -> Result<(), PersistenceError>,
178 ) -> Result<(), DownloadJournalError> {
179 let next = self.next(canister, snapshot, ArtifactStateRecord::Durable, None)?;
180 check_size(&next)?;
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(next, write)
197 }
198
199 fn next(
200 &self,
201 canister: &str,
202 snapshot: &str,
203 state: ArtifactStateRecord,
204 checksum: Option<ArtifactChecksumRecord>,
205 ) -> Result<DownloadJournalRecord, DownloadJournalError> {
206 self.check_usable()?;
207 let mut next = self.record.clone();
208 next.advance(canister, snapshot, state, checksum)?;
209 Ok(next)
210 }
211
212 fn check_usable(&self) -> Result<(), DownloadJournalError> {
213 if !self.usable {
214 return Err(DownloadJournalError::IndeterminateWrite);
215 }
216 self.layout.check_root()?;
217 Ok(())
218 }
219
220 fn check_artifact_parent(&self) -> Result<(), DownloadJournalError> {
221 let path = self.layout.root().join("artifacts");
222 if !fs::symlink_metadata(&path)?.is_dir() {
223 return Err(DownloadJournalError::UnsafeArtifactParent { path });
224 }
225 Ok(())
226 }
227
228 fn store(
229 &mut self,
230 next: DownloadJournalRecord,
231 write: impl FnOnce(&std::path::Path, &DownloadJournalRecord) -> Result<(), PersistenceError>,
232 ) -> Result<(), DownloadJournalError> {
233 check_size(&next)?;
234 self.layout.check_root()?;
235 self.usable = false;
236 write(&self.path(), &next)?;
237 self.record = next;
238 self.usable = true;
239 Ok(())
240 }
241}
242
243fn check_size(record: &DownloadJournalRecord) -> Result<(), PersistenceError> {
244 if serde_json::to_vec_pretty(record)?.len() as u64 > MAX_DOWNLOAD_JOURNAL_BYTES {
245 return Err(PersistenceError::RecordTooLarge {
246 limit: MAX_DOWNLOAD_JOURNAL_BYTES,
247 });
248 }
249 Ok(())
250}
251
252#[derive(Debug, Error)]
254pub enum DownloadJournalError {
255 #[error("download journal immutable intent mismatch")]
257 IntentMismatch,
258 #[error("download journal outcome is indeterminate; reopen retained evidence")]
260 IndeterminateWrite,
261 #[error("unsafe download artifact parent: {path:?}")]
263 UnsafeArtifactParent {
264 path: PathBuf,
266 },
267 #[error(transparent)]
269 Record(#[from] DownloadJournalRecordError),
270 #[error(transparent)]
272 Lock(#[from] JournalLockError),
273 #[error(transparent)]
275 Persistence(#[from] PersistenceError),
276 #[error(transparent)]
278 Artifact(#[from] ArtifactError),
279 #[error(transparent)]
281 Io(#[from] io::Error),
282}
283
284#[cfg(all(test, unix))]
285mod tests;