ic_backup/ops/persistence/restore_reference_release/
mod.rs1use super::{
4 json::{encode_json, write_encoded},
5 layout_lifetime::MAX_RESTORE_REFERENCE_BYTES,
6};
7use crate::{
8 model::restore_references::RestoreReferenceRecord,
9 ops::persistence::{BackupLayoutGuard, JournalLock, PersistenceError},
10 view::restore_reference_release::TerminalRestoreEvidence,
11};
12use std::{fs, path::Path};
13use thiserror::Error;
14
15pub const MAX_RESTORE_TERMINAL_BYTES: usize = 16 * 1024 * 1024;
17
18pub trait RestoreTerminalEvidenceProvider {
25 type Error: std::error::Error + 'static;
27
28 fn qualify_terminal(
33 &self,
34 journal: &Path,
35 bytes: &[u8],
36 ) -> Result<TerminalRestoreEvidence, Self::Error>;
37}
38
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub enum RestoreReferenceReleaseOutcome {
42 Released,
44 AlreadyReleased,
46}
47
48impl BackupLayoutGuard {
49 pub fn release_restore<P: RestoreTerminalEvidenceProvider>(
60 &self,
61 journal_guard: &JournalLock,
62 provider: &P,
63 ) -> Result<RestoreReferenceReleaseOutcome, RestoreReferenceReleaseError<P::Error>> {
64 let journal = journal_guard.journal();
65 self.check_root()?;
66 reject_command_sidecars(journal)?;
67 let bytes = read_terminal(journal)?;
68 let terminal = provider
69 .qualify_terminal(journal, &bytes)
70 .map_err(RestoreReferenceReleaseError::Terminal)?;
71 let reference =
72 RestoreReferenceRecord::new(journal.to_path_buf(), terminal.authority().hash())
73 .map_err(PersistenceError::from)?;
74 let mut references = self.restore_references()?;
75 let released = references
76 .release(&reference)
77 .map_err(PersistenceError::from)?;
78 if read_terminal(journal)? != bytes {
80 return Err(RestoreReferenceReleaseError::JournalChanged);
81 }
82 self.check_root()?;
83 reject_command_sidecars(journal)?;
84 if released {
85 let encoded = encode_json(&references, MAX_RESTORE_REFERENCE_BYTES)?;
86 write_encoded(&self.root().join("restore-references.json"), &encoded)?;
87 }
88 self.sync_references()?;
89 Ok(if released {
90 RestoreReferenceReleaseOutcome::Released
91 } else {
92 RestoreReferenceReleaseOutcome::AlreadyReleased
93 })
94 }
95}
96
97fn reject_command_sidecars<E: std::error::Error + 'static>(
98 journal: &Path,
99) -> Result<(), RestoreReferenceReleaseError<E>> {
100 let parent = journal
101 .parent()
102 .ok_or_else(|| PersistenceError::Io(std::io::ErrorKind::InvalidInput.into()))?;
103 let mut prefix = journal
104 .file_name()
105 .ok_or_else(|| PersistenceError::Io(std::io::ErrorKind::InvalidInput.into()))?
106 .to_os_string();
107 prefix.push(".command-");
108 for entry in fs::read_dir(parent).map_err(PersistenceError::from)? {
109 let name = entry.map_err(PersistenceError::from)?.file_name();
110 if name
111 .as_encoded_bytes()
112 .starts_with(prefix.as_encoded_bytes())
113 {
114 return Err(RestoreReferenceReleaseError::RetiredCommandState);
115 }
116 }
117 Ok(())
118}
119
120fn read_terminal(path: &Path) -> Result<Vec<u8>, PersistenceError> {
121 ic_host_fs::read::read_file_no_follow(path, MAX_RESTORE_TERMINAL_BYTES).map_err(|error| {
122 match error {
123 ic_host_artifacts::artifact::ArtifactError::LimitExceeded { .. } => {
124 PersistenceError::RecordTooLarge {
125 limit: MAX_RESTORE_TERMINAL_BYTES as u64,
126 }
127 }
128 error => PersistenceError::Io(error.into()),
129 }
130 })
131}
132
133#[derive(Debug, Error)]
135pub enum RestoreReferenceReleaseError<E: std::error::Error + 'static> {
136 #[error("restore terminal admission failed: {0}")]
138 Terminal(#[source] E),
139 #[error("command-backed restore requires its original qualified integration")]
141 RetiredCommandState,
142 #[error("restore terminal journal changed during qualification")]
144 JournalChanged,
145 #[error(transparent)]
147 Persistence(#[from] PersistenceError),
148}
149
150#[cfg(all(test, unix))]
151mod tests;