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
7use super::{RestoreApplyJournal, RestoreApplyJournalError, RestorePlan, RestorePlanError};
8use crate::persistence::{PersistenceError, create_json_durable, read_json, write_json_durable};
9
10use std::{io, path::Path};
11
12use thiserror::Error as ThisError;
13
14///
15/// RestorePersistenceError
16///
17/// Typed validation or durable-write failure for restore recovery documents.
18/// Owned by restore persistence and returned to CLI and runner callers.
19///
20
21#[derive(Debug, ThisError)]
22pub enum RestorePersistenceError {
23    #[error("restore apply journal conflicts with the existing recovery document: {path}")]
24    ApplyJournalConflict { path: String },
25
26    #[error(transparent)]
27    InvalidPlan(#[from] RestorePlanError),
28
29    #[error(transparent)]
30    InvalidJournal(#[from] RestoreApplyJournalError),
31
32    #[error(transparent)]
33    Persistence(#[from] PersistenceError),
34
35    #[error("restore plan conflicts with the existing recovery document: {path}")]
36    PlanConflict { path: String },
37}
38
39/// Create a restore plan, or adopt an exact existing plan without replacing it.
40pub fn create_or_adopt_restore_plan(
41    path: &Path,
42    plan: &RestorePlan,
43) -> Result<(), RestorePersistenceError> {
44    plan.validate()?;
45    match read_json::<RestorePlan>(path) {
46        Ok(existing) => {
47            existing.validate()?;
48            if existing == *plan {
49                Ok(())
50            } else {
51                Err(RestorePersistenceError::PlanConflict {
52                    path: path.display().to_string(),
53                })
54            }
55        }
56        Err(PersistenceError::Io(error)) if error.kind() == io::ErrorKind::NotFound => {
57            create_json_durable(path, plan).map_err(RestorePersistenceError::from)
58        }
59        Err(error) => Err(error.into()),
60    }
61}
62
63/// Create a pristine restore journal, or adopt the exact existing journal.
64pub fn create_or_adopt_restore_apply_journal(
65    path: &Path,
66    journal: &RestoreApplyJournal,
67) -> Result<(), RestorePersistenceError> {
68    journal.validate()?;
69    match read_json::<RestoreApplyJournal>(path) {
70        Ok(existing) => {
71            existing.validate()?;
72            if existing == *journal {
73                Ok(())
74            } else {
75                Err(RestorePersistenceError::ApplyJournalConflict {
76                    path: path.display().to_string(),
77                })
78            }
79        }
80        Err(PersistenceError::Io(error)) if error.kind() == io::ErrorKind::NotFound => {
81            create_json_durable(path, journal).map_err(RestorePersistenceError::from)
82        }
83        Err(error) => Err(error.into()),
84    }
85}
86
87/// Durably replace one serialized restore plan.
88pub fn write_restore_plan(path: &Path, plan: &RestorePlan) -> Result<(), RestorePersistenceError> {
89    plan.validate()?;
90    write_json_durable(path, plan)?;
91    Ok(())
92}
93
94/// Validate and durably replace one restore apply journal.
95pub fn write_restore_apply_journal(
96    path: &Path,
97    journal: &RestoreApplyJournal,
98) -> Result<(), RestorePersistenceError> {
99    journal.validate()?;
100    write_json_durable(path, journal)?;
101    Ok(())
102}