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