canic_backup/restore/persistence/
mod.rs1mod retention;
8
9use super::{RestoreApplyJournal, RestoreApplyJournalError, RestorePlan, RestorePlanError};
10use crate::persistence::{
11 BackupLayout, BackupLayoutGuard, JournalLock, JournalLockError, PersistenceError,
12 create_json_durable, read_json, write_json_durable,
13};
14
15use std::{io, path::Path};
16
17use thiserror::Error as ThisError;
18
19pub(in crate::restore) use retention::{lock_restore_layout, release_restore, retain_restore};
20
21#[derive(Debug, ThisError)]
29pub enum RestorePersistenceError {
30 #[error("restore journal backup root does not match the locked layout")]
31 BackupRootMismatch,
32
33 #[error(transparent)]
34 Lock(#[from] JournalLockError),
35
36 #[error("restore apply journal conflicts with the existing recovery document: {path}")]
37 ApplyJournalConflict { path: String },
38
39 #[error(transparent)]
40 InvalidPlan(#[from] RestorePlanError),
41
42 #[error(transparent)]
43 InvalidJournal(#[from] RestoreApplyJournalError),
44
45 #[error(transparent)]
46 Persistence(#[from] PersistenceError),
47
48 #[error("restore plan conflicts with the existing recovery document: {path}")]
49 PlanConflict { path: String },
50}
51
52pub fn create_or_adopt_restore_plan(
54 path: &Path,
55 plan: &RestorePlan,
56) -> Result<(), RestorePersistenceError> {
57 plan.validate()?;
58 match read_json::<RestorePlan>(path) {
59 Ok(existing) => {
60 existing.validate()?;
61 if existing == *plan {
62 Ok(())
63 } else {
64 Err(RestorePersistenceError::PlanConflict {
65 path: path.display().to_string(),
66 })
67 }
68 }
69 Err(PersistenceError::Io(error)) if error.kind() == io::ErrorKind::NotFound => {
70 create_json_durable(path, plan).map_err(RestorePersistenceError::from)
71 }
72 Err(error) => Err(error.into()),
73 }
74}
75
76pub fn create_or_adopt_restore_apply_journal(
78 layout: &BackupLayoutGuard,
79 path: &Path,
80 journal: &RestoreApplyJournal,
81) -> Result<(), RestorePersistenceError> {
82 journal.validate()?;
83 let _lock = lock_journal_for_publication(path)?;
84 match read_json::<RestoreApplyJournal>(path) {
85 Ok(existing) => {
86 existing.validate()?;
87 if existing == *journal {
88 retain_restore(layout, path, journal)?;
89 Ok(())
90 } else {
91 Err(RestorePersistenceError::ApplyJournalConflict {
92 path: path.display().to_string(),
93 })
94 }
95 }
96 Err(PersistenceError::Io(error)) if error.kind() == io::ErrorKind::NotFound => {
97 retain_restore(layout, path, journal)?;
98 create_json_durable(path, journal).map_err(RestorePersistenceError::from)
99 }
100 Err(error) => Err(error.into()),
101 }
102}
103
104pub fn write_restore_plan(path: &Path, plan: &RestorePlan) -> Result<(), RestorePersistenceError> {
106 plan.validate()?;
107 write_json_durable(path, plan)?;
108 Ok(())
109}
110
111pub fn write_restore_apply_journal(
113 path: &Path,
114 journal: &RestoreApplyJournal,
115) -> Result<(), RestorePersistenceError> {
116 journal.validate()?;
117 let layout = journal
120 .backup_root
121 .as_ref()
122 .map(|root| BackupLayout::new(root.into()).lock_lifetime())
123 .transpose()?;
124 let _lock = lock_journal_for_publication(path)?;
125 if let Some(layout) = &layout {
126 retain_restore(layout, path, journal)?;
127 }
128 write_json_durable(path, journal)?;
129 Ok(())
130}
131
132fn lock_journal_for_publication(path: &Path) -> Result<JournalLock, RestorePersistenceError> {
133 let parent = path
134 .parent()
135 .filter(|parent| !parent.as_os_str().is_empty())
136 .unwrap_or_else(|| Path::new("."));
137 std::fs::create_dir_all(parent).map_err(PersistenceError::from)?;
138 Ok(JournalLock::acquire(path)?)
139}