Skip to main content

canic_backup/persistence/
layout.rs

1//! Module: persistence::layout
2//!
3//! Responsibility: define canonical backup layout paths and validated file IO.
4//! Does not own: JSON mechanics, domain validation rules, or checksum verification.
5//! Boundary: exposes filesystem persistence operations for backup workflows.
6
7use crate::{
8    execution::BackupExecutionJournal,
9    journal::DownloadJournal,
10    manifest::DeploymentBackupManifest,
11    persistence::{
12        BackupExecutionIntegrityReport, BackupIntegrityReport, PersistenceError,
13        integrity::{verify_execution_integrity, verify_layout_integrity},
14        json::{create_json_durable, read_json, write_json_durable},
15    },
16    plan::BackupPlan,
17};
18
19use std::path::{Path, PathBuf};
20
21const MANIFEST_FILE_NAME: &str = "deployment-backup-manifest.json";
22const BACKUP_PLAN_FILE_NAME: &str = "backup-plan.json";
23const JOURNAL_FILE_NAME: &str = "download-journal.json";
24const EXECUTION_JOURNAL_FILE_NAME: &str = "backup-execution-journal.json";
25
26///
27/// BackupLayout
28///
29/// Canonical filesystem layout for one backup directory.
30/// Owned by persistence and used by backup runner and restore verification paths.
31///
32
33#[derive(Clone, Debug)]
34pub struct BackupLayout {
35    root: PathBuf,
36}
37
38impl BackupLayout {
39    /// Create a filesystem layout rooted at one backup directory.
40    #[must_use]
41    pub const fn new(root: PathBuf) -> Self {
42        Self { root }
43    }
44
45    /// Return the root backup directory path.
46    #[must_use]
47    pub fn root(&self) -> &Path {
48        &self.root
49    }
50
51    /// Return the canonical manifest path for this backup layout.
52    #[must_use]
53    pub fn manifest_path(&self) -> PathBuf {
54        self.root.join(MANIFEST_FILE_NAME)
55    }
56
57    /// Return the canonical backup plan path for this layout.
58    #[must_use]
59    pub fn backup_plan_path(&self) -> PathBuf {
60        self.root.join(BACKUP_PLAN_FILE_NAME)
61    }
62
63    /// Return the canonical mutable journal path for this backup layout.
64    #[must_use]
65    pub fn journal_path(&self) -> PathBuf {
66        self.root.join(JOURNAL_FILE_NAME)
67    }
68
69    /// Return the canonical backup execution journal path for this layout.
70    #[must_use]
71    pub fn execution_journal_path(&self) -> PathBuf {
72        self.root.join(EXECUTION_JOURNAL_FILE_NAME)
73    }
74
75    /// Publish a validated manifest or adopt the exact existing manifest.
76    pub fn publish_manifest(
77        &self,
78        manifest: &DeploymentBackupManifest,
79    ) -> Result<(), PersistenceError> {
80        manifest.validate()?;
81        let path = self.manifest_path();
82        if path.exists() {
83            return self.require_exact_manifest(manifest);
84        }
85        match create_json_durable(&path, manifest) {
86            Ok(()) => Ok(()),
87            Err(PersistenceError::Io(error))
88                if error.kind() == std::io::ErrorKind::AlreadyExists =>
89            {
90                self.require_exact_manifest(manifest)
91            }
92            Err(error) => Err(error),
93        }
94    }
95
96    /// Read and validate a manifest from this backup layout.
97    pub fn read_manifest(&self) -> Result<DeploymentBackupManifest, PersistenceError> {
98        let manifest = read_json(&self.manifest_path())?;
99        DeploymentBackupManifest::validate(&manifest)?;
100        Ok(manifest)
101    }
102
103    fn require_exact_manifest(
104        &self,
105        expected: &DeploymentBackupManifest,
106    ) -> Result<(), PersistenceError> {
107        let path = self.manifest_path();
108        let metadata = std::fs::symlink_metadata(&path)?;
109        if metadata.file_type().is_symlink() || !metadata.is_file() {
110            return Err(PersistenceError::ManifestConflict {
111                path: path.display().to_string(),
112            });
113        }
114        let actual = self.read_manifest()?;
115        if serde_json::to_vec(&actual)? == serde_json::to_vec(expected)? {
116            std::fs::File::open(&path)?.sync_all()?;
117            if let Some(parent) = path.parent() {
118                std::fs::File::open(parent)?.sync_all()?;
119            }
120            Ok(())
121        } else {
122            Err(PersistenceError::ManifestConflict {
123                path: path.display().to_string(),
124            })
125        }
126    }
127
128    #[cfg(test)]
129    pub(crate) fn publish_manifest_at_barriers(
130        &self,
131        manifest: &DeploymentBackupManifest,
132        before_publication: impl FnMut(),
133        after_directory_sync: impl FnMut(),
134    ) -> Result<(), PersistenceError> {
135        use crate::persistence::json::create_json_durable_at_barriers;
136
137        manifest.validate()?;
138        create_json_durable_at_barriers(
139            &self.manifest_path(),
140            manifest,
141            before_publication,
142            after_directory_sync,
143        )
144    }
145
146    /// Write a validated backup plan with durable replace semantics.
147    pub fn write_backup_plan(&self, plan: &BackupPlan) -> Result<(), PersistenceError> {
148        plan.validate()?;
149        write_json_durable(&self.backup_plan_path(), plan)
150    }
151
152    /// Read and validate a backup plan from this layout.
153    pub fn read_backup_plan(&self) -> Result<BackupPlan, PersistenceError> {
154        let plan = read_json(&self.backup_plan_path())?;
155        BackupPlan::validate(&plan)?;
156        Ok(plan)
157    }
158
159    /// Write a validated download journal with durable replace semantics.
160    pub fn write_journal(&self, journal: &DownloadJournal) -> Result<(), PersistenceError> {
161        journal.validate()?;
162        write_json_durable(&self.journal_path(), journal)
163    }
164
165    /// Read and validate a download journal from this backup layout.
166    pub fn read_journal(&self) -> Result<DownloadJournal, PersistenceError> {
167        let journal = read_json(&self.journal_path())?;
168        DownloadJournal::validate(&journal)?;
169        Ok(journal)
170    }
171
172    /// Write a validated backup execution journal with durable replace semantics.
173    pub fn write_execution_journal(
174        &self,
175        journal: &BackupExecutionJournal,
176    ) -> Result<(), PersistenceError> {
177        journal.validate()?;
178        write_json_durable(&self.execution_journal_path(), journal)
179    }
180
181    #[cfg(all(test, unix))]
182    pub(crate) fn write_execution_journal_at_barriers(
183        &self,
184        journal: &BackupExecutionJournal,
185        barriers: impl FnMut(crate::persistence::DurableWriteBarrier),
186    ) -> Result<(), PersistenceError> {
187        use crate::persistence::write_json_durable_at_barriers;
188
189        journal.validate()?;
190        write_json_durable_at_barriers(&self.execution_journal_path(), journal, barriers)
191    }
192
193    /// Read and validate a backup execution journal from this layout.
194    pub fn read_execution_journal(&self) -> Result<BackupExecutionJournal, PersistenceError> {
195        let journal = read_json(&self.execution_journal_path())?;
196        BackupExecutionJournal::validate(&journal)?;
197        Ok(journal)
198    }
199
200    /// Validate the manifest, journal, and durable artifact checksums.
201    pub fn verify_integrity(&self) -> Result<BackupIntegrityReport, PersistenceError> {
202        let manifest = self.read_manifest()?;
203        let journal = self.read_journal()?;
204        verify_layout_integrity(self, &manifest, &journal)
205    }
206
207    /// Validate the persisted backup plan and execution journal agree.
208    pub fn verify_execution_integrity(
209        &self,
210    ) -> Result<BackupExecutionIntegrityReport, PersistenceError> {
211        let plan = self.read_backup_plan()?;
212        let journal = self.read_execution_journal()?;
213        verify_execution_integrity(&plan, &journal)
214    }
215}