ic_backup/ops/persistence/download_journal/
mod.rs1mod integrity;
4pub use integrity::DownloadIntegrityError;
5
6use super::{
7 BackupLayoutGuard, JournalLock, JournalLockError, PersistenceError, commit_artifact_directory,
8 create_json_durable, read_json, write_json_durable,
9};
10use crate::{
11 model::{
12 artifacts::ArtifactChecksumRecord,
13 download_journal::{
14 ArtifactStateRecord, DownloadArtifactRequest, DownloadJournalRecord,
15 DownloadJournalRecordError, MAX_DOWNLOAD_JOURNAL_BYTES,
16 },
17 },
18 ops::artifacts::{ArtifactError, checksum_directory},
19};
20use std::{fs, io, path::PathBuf};
21use thiserror::Error;
22
23const JOURNAL_FILE: &str = "download-journal.json";
24
25#[derive(Debug)]
30pub struct DownloadJournalGuard<'a> {
31 layout: &'a BackupLayoutGuard,
32 _lock: JournalLock,
33 record: DownloadJournalRecord,
34 usable: bool,
35}
36
37impl<'a> DownloadJournalGuard<'a> {
38 pub fn create(
43 layout: &'a BackupLayoutGuard,
44 intent: &str,
45 artifacts: Vec<DownloadArtifactRequest>,
46 ) -> Result<Self, DownloadJournalError> {
47 layout.check_root()?;
48 let path = layout.root().join(JOURNAL_FILE);
49 let lock = JournalLock::acquire(&path)?;
50 let record = DownloadJournalRecord::new(intent, artifacts)?;
51 check_size(&record)?;
52 create_json_durable(&path, &record)?;
53 Ok(Self {
54 layout,
55 _lock: lock,
56 record,
57 usable: true,
58 })
59 }
60
61 pub fn open(
67 layout: &'a BackupLayoutGuard,
68 expected_intent: &str,
69 ) -> Result<Self, DownloadJournalError> {
70 layout.check_root()?;
71 let expected = ArtifactChecksumRecord::from_hash(expected_intent)
72 .map_err(DownloadJournalRecordError::from)?;
73 let path = layout.root().join(JOURNAL_FILE);
74 let lock = JournalLock::acquire(&path)?;
75 let record: DownloadJournalRecord = read_json(&path, MAX_DOWNLOAD_JOURNAL_BYTES)?;
76 check_size(&record)?;
77 if record.intent() != expected.hash() {
78 return Err(DownloadJournalError::IntentMismatch);
79 }
80 Ok(Self {
81 layout,
82 _lock: lock,
83 record,
84 usable: true,
85 })
86 }
87
88 pub fn record(&self) -> Result<&DownloadJournalRecord, DownloadJournalError> {
93 self.check_usable()?;
94 Ok(&self.record)
95 }
96
97 #[must_use]
99 pub fn path(&self) -> PathBuf {
100 self.layout.root().join(JOURNAL_FILE)
101 }
102
103 pub fn record_downloaded(
111 &mut self,
112 canister: &str,
113 snapshot: &str,
114 ) -> Result<(), DownloadJournalError> {
115 let next = self.next(canister, snapshot, ArtifactStateRecord::Downloaded, None)?;
116 self.check_artifact_parent()?;
117 let entry = next.artifact(canister, snapshot)?;
118 checksum_directory(&self.layout.root().join(entry.staging_path()))?;
119 self.store(next, write_json_durable)
120 }
121
122 pub fn verify_artifact(
127 &mut self,
128 canister: &str,
129 snapshot: &str,
130 ) -> Result<(), DownloadJournalError> {
131 self.check_usable()?;
132 let entry = self.record.artifact(canister, snapshot)?;
133 if entry.state() != ArtifactStateRecord::Downloaded {
134 return Err(DownloadJournalRecordError::InvalidStateTransition {
135 from: entry.state(),
136 to: ArtifactStateRecord::ChecksumVerified,
137 }
138 .into());
139 }
140 self.check_artifact_parent()?;
141 let checksum = checksum_directory(&self.layout.root().join(entry.staging_path()))?;
142 let next = self.next(
143 canister,
144 snapshot,
145 ArtifactStateRecord::ChecksumVerified,
146 Some(checksum),
147 )?;
148 self.store(next, write_json_durable)
149 }
150
151 pub fn finalize_artifact(
158 &mut self,
159 canister: &str,
160 snapshot: &str,
161 ) -> Result<(), DownloadJournalError> {
162 self.finalize_with(canister, snapshot, write_json_durable)
163 }
164
165 fn finalize_with(
166 &mut self,
167 canister: &str,
168 snapshot: &str,
169 write: impl FnOnce(&std::path::Path, &DownloadJournalRecord) -> Result<(), PersistenceError>,
170 ) -> Result<(), DownloadJournalError> {
171 let next = self.next(canister, snapshot, ArtifactStateRecord::Durable, None)?;
172 check_size(&next)?;
173 self.check_artifact_parent()?;
174 let entry = next.artifact(canister, snapshot)?;
175 let checksum = entry
176 .checksum()
177 .ok_or(DownloadJournalRecordError::InvalidChecksumState(
178 ArtifactStateRecord::Durable,
179 ))?;
180 self.usable = false;
183 commit_artifact_directory(
184 &self.layout.root().join(entry.staging_path()),
185 &self.layout.root().join(entry.artifact_path()),
186 checksum.hash(),
187 )?;
188 self.store(next, write)
189 }
190
191 fn next(
192 &self,
193 canister: &str,
194 snapshot: &str,
195 state: ArtifactStateRecord,
196 checksum: Option<ArtifactChecksumRecord>,
197 ) -> Result<DownloadJournalRecord, DownloadJournalError> {
198 self.check_usable()?;
199 let mut next = self.record.clone();
200 next.advance(canister, snapshot, state, checksum)?;
201 Ok(next)
202 }
203
204 fn check_usable(&self) -> Result<(), DownloadJournalError> {
205 if !self.usable {
206 return Err(DownloadJournalError::IndeterminateWrite);
207 }
208 self.layout.check_root()?;
209 Ok(())
210 }
211
212 fn check_artifact_parent(&self) -> Result<(), DownloadJournalError> {
213 let path = self.layout.root().join("artifacts");
214 if !fs::symlink_metadata(&path)?.is_dir() {
215 return Err(DownloadJournalError::UnsafeArtifactParent { path });
216 }
217 Ok(())
218 }
219
220 fn store(
221 &mut self,
222 next: DownloadJournalRecord,
223 write: impl FnOnce(&std::path::Path, &DownloadJournalRecord) -> Result<(), PersistenceError>,
224 ) -> Result<(), DownloadJournalError> {
225 check_size(&next)?;
226 self.layout.check_root()?;
227 self.usable = false;
228 write(&self.path(), &next)?;
229 self.record = next;
230 self.usable = true;
231 Ok(())
232 }
233}
234
235fn check_size(record: &DownloadJournalRecord) -> Result<(), PersistenceError> {
236 if serde_json::to_vec_pretty(record)?.len() as u64 > MAX_DOWNLOAD_JOURNAL_BYTES {
237 return Err(PersistenceError::RecordTooLarge {
238 limit: MAX_DOWNLOAD_JOURNAL_BYTES,
239 });
240 }
241 Ok(())
242}
243
244#[derive(Debug, Error)]
246pub enum DownloadJournalError {
247 #[error("download journal immutable intent mismatch")]
249 IntentMismatch,
250 #[error("download journal outcome is indeterminate; reopen retained evidence")]
252 IndeterminateWrite,
253 #[error("unsafe download artifact parent: {path:?}")]
255 UnsafeArtifactParent {
256 path: PathBuf,
258 },
259 #[error(transparent)]
261 Record(#[from] DownloadJournalRecordError),
262 #[error(transparent)]
264 Lock(#[from] JournalLockError),
265 #[error(transparent)]
267 Persistence(#[from] PersistenceError),
268 #[error(transparent)]
270 Artifact(#[from] ArtifactError),
271 #[error(transparent)]
273 Io(#[from] io::Error),
274}
275
276#[cfg(all(test, unix))]
277mod tests;