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