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        BackupExecutionGuard, BackupExecutionIntegrityReport, BackupIntegrityReport,
13        BackupLayoutGuard, JournalLock, JournalLockError, PersistenceError,
14        integrity::{verify_execution_integrity, verify_layout_integrity},
15        json::{create_json_durable, read_json, write_json_durable},
16    },
17    plan::BackupPlan,
18};
19
20use std::path::{Path, PathBuf};
21
22const MANIFEST_FILE_NAME: &str = "deployment-backup-manifest.json";
23const BACKUP_PLAN_FILE_NAME: &str = "backup-plan.json";
24const JOURNAL_FILE_NAME: &str = "download-journal.json";
25const EXECUTION_JOURNAL_FILE_NAME: &str = "backup-execution-journal.json";
26
27///
28/// BackupLayout
29///
30/// Canonical filesystem layout for one backup directory.
31/// Owned by persistence and used by backup runner and restore verification paths.
32///
33
34#[derive(Clone, Debug)]
35pub struct BackupLayout {
36    root: PathBuf,
37}
38
39impl BackupLayout {
40    /// Create a filesystem layout rooted at one backup directory.
41    #[must_use]
42    pub const fn new(root: PathBuf) -> Self {
43        Self { root }
44    }
45
46    /// Resolve an existing operator-selected root before deriving executor and artifact paths.
47    pub(crate) fn resolve_root(&self) -> Result<Self, PersistenceError> {
48        let root = self.root.canonicalize()?;
49        if !root.is_dir() {
50            return Err(std::io::Error::from(std::io::ErrorKind::NotADirectory).into());
51        }
52        Ok(Self::new(root))
53    }
54
55    /// Return the root backup directory path.
56    #[must_use]
57    pub fn root(&self) -> &Path {
58        &self.root
59    }
60
61    /// Return the canonical manifest path for this backup layout.
62    #[must_use]
63    pub fn manifest_path(&self) -> PathBuf {
64        self.root.join(MANIFEST_FILE_NAME)
65    }
66
67    /// Return the canonical backup plan path for this layout.
68    #[must_use]
69    pub fn backup_plan_path(&self) -> PathBuf {
70        self.root.join(BACKUP_PLAN_FILE_NAME)
71    }
72
73    /// Return the canonical mutable journal path for this backup layout.
74    #[must_use]
75    pub fn journal_path(&self) -> PathBuf {
76        self.root.join(JOURNAL_FILE_NAME)
77    }
78
79    /// Return the canonical backup execution journal path for this layout.
80    #[must_use]
81    pub fn execution_journal_path(&self) -> PathBuf {
82        self.root.join(EXECUTION_JOURNAL_FILE_NAME)
83    }
84
85    /// Serialize plan and execution-journal initialization with runner mutation.
86    /// The backup directory must exist; release this guard before invoking the runner.
87    pub fn lock_execution(&self) -> Result<BackupExecutionGuard, JournalLockError> {
88        let lifetime = self.lock_lifetime()?;
89        let journal = JournalLock::acquire(&lifetime.root().join(EXECUTION_JOURNAL_FILE_NAME))?;
90        Ok(BackupExecutionGuard {
91            _lifetime: lifetime,
92            _journal: journal,
93        })
94    }
95
96    /// Exclude backup creation, execution, restore and prune for this existing layout.
97    pub fn lock_lifetime(&self) -> Result<BackupLayoutGuard, JournalLockError> {
98        BackupLayoutGuard::acquire(&self.root)
99    }
100
101    /// Publish a validated manifest or adopt the exact existing manifest.
102    pub fn publish_manifest(
103        &self,
104        manifest: &DeploymentBackupManifest,
105    ) -> Result<(), PersistenceError> {
106        manifest.validate()?;
107        let path = self.manifest_path();
108        if path.exists() {
109            return self.require_exact_manifest(manifest);
110        }
111        match create_json_durable(&path, manifest) {
112            Ok(()) => Ok(()),
113            Err(PersistenceError::Io(error))
114                if error.kind() == std::io::ErrorKind::AlreadyExists =>
115            {
116                self.require_exact_manifest(manifest)
117            }
118            Err(error) => Err(error),
119        }
120    }
121
122    /// Read and validate a manifest from this backup layout.
123    pub fn read_manifest(&self) -> Result<DeploymentBackupManifest, PersistenceError> {
124        let path = self.manifest_path();
125        let metadata = std::fs::symlink_metadata(&path)?;
126        if metadata.file_type().is_symlink() || !metadata.is_file() {
127            return Err(PersistenceError::ManifestConflict {
128                path: path.display().to_string(),
129            });
130        }
131        let manifest = read_json(&path)?;
132        DeploymentBackupManifest::validate(&manifest)?;
133        Ok(manifest)
134    }
135
136    fn require_exact_manifest(
137        &self,
138        expected: &DeploymentBackupManifest,
139    ) -> Result<(), PersistenceError> {
140        let path = self.manifest_path();
141        let actual = self.read_manifest()?;
142        if serde_json::to_vec(&actual)? == serde_json::to_vec(expected)? {
143            std::fs::File::open(&path)?.sync_all()?;
144            if let Some(parent) = path.parent() {
145                std::fs::File::open(parent)?.sync_all()?;
146            }
147            Ok(())
148        } else {
149            Err(PersistenceError::ManifestConflict {
150                path: path.display().to_string(),
151            })
152        }
153    }
154
155    #[cfg(test)]
156    pub(crate) fn publish_manifest_at_barriers(
157        &self,
158        manifest: &DeploymentBackupManifest,
159        before_publication: impl FnMut(),
160        after_directory_sync: impl FnMut(),
161    ) -> Result<(), PersistenceError> {
162        use crate::persistence::json::create_json_durable_at_barriers;
163
164        manifest.validate()?;
165        create_json_durable_at_barriers(
166            &self.manifest_path(),
167            manifest,
168            before_publication,
169            after_directory_sync,
170        )
171    }
172
173    /// Write a validated backup plan with durable replace semantics.
174    pub fn write_backup_plan(&self, plan: &BackupPlan) -> Result<(), PersistenceError> {
175        plan.validate()?;
176        write_json_durable(&self.backup_plan_path(), plan)
177    }
178
179    /// Read and validate a backup plan from this layout.
180    pub fn read_backup_plan(&self) -> Result<BackupPlan, PersistenceError> {
181        let plan = read_json(&self.backup_plan_path())?;
182        BackupPlan::validate(&plan)?;
183        Ok(plan)
184    }
185
186    /// Write a validated download journal with durable replace semantics.
187    pub fn write_journal(&self, journal: &DownloadJournal) -> Result<(), PersistenceError> {
188        journal.validate()?;
189        write_json_durable(&self.journal_path(), journal)
190    }
191
192    /// Read and validate a download journal from this backup layout.
193    pub fn read_journal(&self) -> Result<DownloadJournal, PersistenceError> {
194        let journal = read_json(&self.journal_path())?;
195        DownloadJournal::validate(&journal)?;
196        Ok(journal)
197    }
198
199    /// Write a validated backup execution journal with durable replace semantics.
200    pub fn write_execution_journal(
201        &self,
202        journal: &BackupExecutionJournal,
203    ) -> Result<(), PersistenceError> {
204        journal.validate()?;
205        write_json_durable(&self.execution_journal_path(), journal)
206    }
207
208    #[cfg(all(test, unix))]
209    pub(crate) fn write_execution_journal_at_barriers(
210        &self,
211        journal: &BackupExecutionJournal,
212        barriers: impl FnMut(crate::persistence::DurableWriteBarrier),
213    ) -> Result<(), PersistenceError> {
214        use crate::persistence::write_json_durable_at_barriers;
215
216        journal.validate()?;
217        write_json_durable_at_barriers(&self.execution_journal_path(), journal, barriers)
218    }
219
220    /// Read and validate a backup execution journal from this layout.
221    pub fn read_execution_journal(&self) -> Result<BackupExecutionJournal, PersistenceError> {
222        let journal = read_json(&self.execution_journal_path())?;
223        BackupExecutionJournal::validate(&journal)?;
224        Ok(journal)
225    }
226
227    /// Validate the manifest, journal, and durable artifact checksums.
228    pub fn verify_integrity(&self) -> Result<BackupIntegrityReport, PersistenceError> {
229        let layout = self.resolve_root()?;
230        let manifest = layout.read_manifest()?;
231        let journal = layout.read_journal()?;
232        verify_layout_integrity(&layout, &manifest, &journal)
233    }
234
235    /// Validate the persisted backup plan and execution journal agree.
236    pub fn verify_execution_integrity(
237        &self,
238    ) -> Result<BackupExecutionIntegrityReport, PersistenceError> {
239        let plan = self.read_backup_plan()?;
240        let journal = self.read_execution_journal()?;
241        verify_execution_integrity(&plan, &journal)
242    }
243}