mod integrity;
mod local_restore_artifact;
mod local_restore_source;
mod manifest;
pub use integrity::DownloadIntegrityError;
pub use local_restore_artifact::{LocalRestoreArtifactError, LocalRestoreArtifactView};
pub use local_restore_source::LocalRestoreSourceError;
pub use manifest::{DownloadManifestError, read_download_manifest};
use super::{
BackupLayoutGuard, JournalLock, JournalLockError, PersistenceError, commit_artifact_directory,
create_json_durable, read_json, write_json_durable,
};
use crate::{
model::{
artifacts::ArtifactChecksumRecord,
download_journal::{
ArtifactStateRecord, DownloadArtifactRequest, DownloadJournalRecord,
DownloadJournalRecordError, MAX_DOWNLOAD_JOURNAL_BYTES,
},
},
ops::artifacts::{ArtifactError, checksum_directory},
};
use std::{fs, io, path::PathBuf};
use thiserror::Error;
const JOURNAL_FILE: &str = "download-journal.json";
#[derive(Debug)]
pub struct DownloadJournalGuard<'a> {
layout: &'a BackupLayoutGuard,
_lock: JournalLock,
record: DownloadJournalRecord,
usable: bool,
}
impl<'a> DownloadJournalGuard<'a> {
pub fn create(
layout: &'a BackupLayoutGuard,
intent: &str,
artifacts: Vec<DownloadArtifactRequest>,
) -> Result<Self, DownloadJournalError> {
layout.check_root()?;
let path = layout.root().join(JOURNAL_FILE);
let lock = JournalLock::acquire(&path)?;
let record = DownloadJournalRecord::new(intent, artifacts)?;
check_size(&record)?;
create_json_durable(&path, &record)?;
Ok(Self {
layout,
_lock: lock,
record,
usable: true,
})
}
pub fn open(
layout: &'a BackupLayoutGuard,
expected_intent: &str,
) -> Result<Self, DownloadJournalError> {
layout.check_root()?;
let expected = ArtifactChecksumRecord::from_hash(expected_intent)
.map_err(DownloadJournalRecordError::from)?;
let path = layout.root().join(JOURNAL_FILE);
let lock = JournalLock::acquire(&path)?;
let record: DownloadJournalRecord = read_json(&path, MAX_DOWNLOAD_JOURNAL_BYTES)?;
check_size(&record)?;
if record.intent() != expected.hash() {
return Err(DownloadJournalError::IntentMismatch);
}
Ok(Self {
layout,
_lock: lock,
record,
usable: true,
})
}
pub fn record(&self) -> Result<&DownloadJournalRecord, DownloadJournalError> {
self.check_usable()?;
Ok(&self.record)
}
#[must_use]
pub fn path(&self) -> PathBuf {
self.layout.root().join(JOURNAL_FILE)
}
pub fn record_downloaded(
&mut self,
canister: &str,
snapshot: &str,
) -> Result<(), DownloadJournalError> {
let next = self.next(canister, snapshot, ArtifactStateRecord::Downloaded, None)?;
self.check_artifact_parent()?;
let entry = next.artifact(canister, snapshot)?;
checksum_directory(&self.layout.root().join(entry.staging_path()))?;
self.store(next, write_json_durable)
}
pub fn verify_artifact(
&mut self,
canister: &str,
snapshot: &str,
) -> Result<(), DownloadJournalError> {
self.check_usable()?;
let entry = self.record.artifact(canister, snapshot)?;
if entry.state() != ArtifactStateRecord::Downloaded {
return Err(DownloadJournalRecordError::InvalidStateTransition {
from: entry.state(),
to: ArtifactStateRecord::ChecksumVerified,
}
.into());
}
self.check_artifact_parent()?;
let checksum = checksum_directory(&self.layout.root().join(entry.staging_path()))?;
let next = self.next(
canister,
snapshot,
ArtifactStateRecord::ChecksumVerified,
Some(checksum),
)?;
self.store(next, write_json_durable)
}
pub fn finalize_artifact(
&mut self,
canister: &str,
snapshot: &str,
) -> Result<(), DownloadJournalError> {
self.finalize_with(canister, snapshot, write_json_durable)
}
fn finalize_with(
&mut self,
canister: &str,
snapshot: &str,
write: impl FnOnce(&std::path::Path, &DownloadJournalRecord) -> Result<(), PersistenceError>,
) -> Result<(), DownloadJournalError> {
let next = self.next(canister, snapshot, ArtifactStateRecord::Durable, None)?;
check_size(&next)?;
self.check_artifact_parent()?;
let entry = next.artifact(canister, snapshot)?;
let checksum = entry
.checksum()
.ok_or(DownloadJournalRecordError::InvalidChecksumState(
ArtifactStateRecord::Durable,
))?;
self.usable = false;
commit_artifact_directory(
&self.layout.root().join(entry.staging_path()),
&self.layout.root().join(entry.artifact_path()),
checksum.hash(),
)?;
self.store(next, write)
}
fn next(
&self,
canister: &str,
snapshot: &str,
state: ArtifactStateRecord,
checksum: Option<ArtifactChecksumRecord>,
) -> Result<DownloadJournalRecord, DownloadJournalError> {
self.check_usable()?;
let mut next = self.record.clone();
next.advance(canister, snapshot, state, checksum)?;
Ok(next)
}
fn check_usable(&self) -> Result<(), DownloadJournalError> {
if !self.usable {
return Err(DownloadJournalError::IndeterminateWrite);
}
self.layout.check_root()?;
Ok(())
}
fn check_artifact_parent(&self) -> Result<(), DownloadJournalError> {
let path = self.layout.root().join("artifacts");
if !fs::symlink_metadata(&path)?.is_dir() {
return Err(DownloadJournalError::UnsafeArtifactParent { path });
}
Ok(())
}
fn store(
&mut self,
next: DownloadJournalRecord,
write: impl FnOnce(&std::path::Path, &DownloadJournalRecord) -> Result<(), PersistenceError>,
) -> Result<(), DownloadJournalError> {
check_size(&next)?;
self.layout.check_root()?;
self.usable = false;
write(&self.path(), &next)?;
self.record = next;
self.usable = true;
Ok(())
}
}
fn check_size(record: &DownloadJournalRecord) -> Result<(), PersistenceError> {
if serde_json::to_vec_pretty(record)?.len() as u64 > MAX_DOWNLOAD_JOURNAL_BYTES {
return Err(PersistenceError::RecordTooLarge {
limit: MAX_DOWNLOAD_JOURNAL_BYTES,
});
}
Ok(())
}
#[derive(Debug, Error)]
pub enum DownloadJournalError {
#[error("download journal immutable intent mismatch")]
IntentMismatch,
#[error("download journal outcome is indeterminate; reopen retained evidence")]
IndeterminateWrite,
#[error("unsafe download artifact parent: {path:?}")]
UnsafeArtifactParent {
path: PathBuf,
},
#[error(transparent)]
Record(#[from] DownloadJournalRecordError),
#[error(transparent)]
Lock(#[from] JournalLockError),
#[error(transparent)]
Persistence(#[from] PersistenceError),
#[error(transparent)]
Artifact(#[from] ArtifactError),
#[error(transparent)]
Io(#[from] io::Error),
}
#[cfg(all(test, unix))]
mod tests;