Skip to main content

canic_backup/restore/persistence/
mod.rs

1//! Module: restore::persistence
2//!
3//! Responsibility: validate and durably persist restore recovery documents.
4//! Does not own: restore planning, journal transitions, or generic CLI output.
5//! Boundary: exposes typed plan/journal writes backed by backup-owned durable IO.
6
7mod 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///
22/// RestorePersistenceError
23///
24/// Typed validation or durable-write failure for restore recovery documents.
25/// Owned by restore persistence and returned to CLI and runner callers.
26///
27
28#[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
52/// Create a restore plan, or adopt an exact existing plan without replacing it.
53pub 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
76/// Create a pristine restore journal, or adopt the exact existing journal.
77pub 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
104/// Durably replace one serialized restore plan.
105pub fn write_restore_plan(path: &Path, plan: &RestorePlan) -> Result<(), RestorePersistenceError> {
106    plan.validate()?;
107    write_json_durable(path, plan)?;
108    Ok(())
109}
110
111/// Validate and durably replace one restore apply journal.
112pub fn write_restore_apply_journal(
113    path: &Path,
114    journal: &RestoreApplyJournal,
115) -> Result<(), RestorePersistenceError> {
116    journal.validate()?;
117    // Acquire the existing source before creating a journal parent. Otherwise a
118    // default journal path could recreate a backup that prune has already removed.
119    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}