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