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