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 verify_artifact(
118 &mut self,
119 canister: &str,
120 snapshot: &str,
121 ) -> Result<(), DownloadJournalError> {
122 self.check_usable()?;
123 let entry = self.record.artifact(canister, snapshot)?;
124 if entry.state() != ArtifactStateRecord::Downloaded {
125 return Err(DownloadJournalRecordError::InvalidStateTransition {
126 from: entry.state(),
127 to: ArtifactStateRecord::ChecksumVerified,
128 }
129 .into());
130 }
131 self.check_artifact_parent()?;
132 let checksum = checksum_directory(&self.layout.root().join(entry.staging_path()))?;
133 let next = self.next(
134 canister,
135 snapshot,
136 ArtifactTransition::ChecksumVerified(checksum),
137 )?;
138 self.store(next, write_encoded)
139 }
140
141 pub fn finalize_artifact(
148 &mut self,
149 canister: &str,
150 snapshot: &str,
151 ) -> Result<(), DownloadJournalError> {
152 self.finalize_with(canister, snapshot, write_encoded)
153 }
154
155 fn finalize_with(
156 &mut self,
157 canister: &str,
158 snapshot: &str,
159 write: impl FnOnce(&std::path::Path, &[u8]) -> Result<(), PersistenceError>,
160 ) -> Result<(), DownloadJournalError> {
161 let next = self.next(canister, snapshot, ArtifactTransition::Durable)?;
162 let bytes = encode_json(&next, MAX_DOWNLOAD_JOURNAL_BYTES)?;
163 self.check_artifact_parent()?;
164 let entry = next.artifact(canister, snapshot)?;
165 let checksum = entry
166 .checksum()
167 .ok_or(DownloadJournalRecordError::InvalidChecksumState(
168 ArtifactStateRecord::Durable,
169 ))?;
170 self.usable = false;
173 commit_artifact_directory(
174 &self.layout.root().join(entry.staging_path()),
175 &self.layout.root().join(entry.artifact_path()),
176 checksum.hash(),
177 )?;
178 self.store_encoded(next, &bytes, write)
179 }
180
181 fn next(
182 &self,
183 canister: &str,
184 snapshot: &str,
185 transition: ArtifactTransition,
186 ) -> Result<DownloadJournalRecord, DownloadJournalError> {
187 self.check_usable()?;
188 let mut next = self.record.clone();
189 next.advance(canister, snapshot, transition)?;
190 Ok(next)
191 }
192
193 fn check_usable(&self) -> Result<(), DownloadJournalError> {
194 if !self.usable {
195 return Err(DownloadJournalError::IndeterminateWrite);
196 }
197 self.layout.check_root()?;
198 Ok(())
199 }
200
201 fn check_artifact_parent(&self) -> Result<(), DownloadJournalError> {
202 let path = self.layout.root().join("artifacts");
203 if !fs::symlink_metadata(&path)?.is_dir() {
204 return Err(DownloadJournalError::UnsafeArtifactParent { path });
205 }
206 Ok(())
207 }
208
209 fn store(
210 &mut self,
211 next: DownloadJournalRecord,
212 write: impl FnOnce(&std::path::Path, &[u8]) -> Result<(), PersistenceError>,
213 ) -> Result<(), DownloadJournalError> {
214 let bytes = encode_json(&next, MAX_DOWNLOAD_JOURNAL_BYTES)?;
215 self.store_encoded(next, &bytes, write)
216 }
217
218 fn store_encoded(
219 &mut self,
220 next: DownloadJournalRecord,
221 bytes: &[u8],
222 write: impl FnOnce(&std::path::Path, &[u8]) -> Result<(), PersistenceError>,
223 ) -> Result<(), DownloadJournalError> {
224 self.layout.check_root()?;
225 let path = self.path();
226 super::json::replace_cached_record(&mut self.record, &mut self.usable, next, || {
227 write(&path, bytes)
228 })?;
229 Ok(())
230 }
231}
232
233#[derive(Debug, Error)]
235pub enum DownloadJournalError {
236 #[error("download journal immutable intent mismatch")]
238 IntentMismatch,
239 #[error("download journal outcome is indeterminate; reopen retained evidence")]
241 IndeterminateWrite,
242 #[error("unsafe download artifact parent: {path:?}")]
244 UnsafeArtifactParent {
245 path: PathBuf,
247 },
248 #[error(transparent)]
250 Record(#[from] DownloadJournalRecordError),
251 #[error(transparent)]
253 Lock(#[from] JournalLockError),
254 #[error(transparent)]
256 Persistence(#[from] PersistenceError),
257 #[error(transparent)]
259 Artifact(#[from] ArtifactError),
260 #[error(transparent)]
262 Io(#[from] io::Error),
263}
264
265#[cfg(all(test, unix))]
266mod tests;