canic_backup/persistence/layout_lifetime/
mod.rs1#[cfg(test)]
8mod tests;
9
10use crate::persistence::{
11 JournalLock, JournalLockError, PersistenceError, read_json, write_json_durable,
12};
13
14use std::{
15 fs, io,
16 path::{Path, PathBuf},
17};
18
19use serde::{Deserialize, Serialize};
20use sha2::{Digest, Sha256};
21
22const REFERENCES_FILE: &str = "restore-references.json";
23
24#[derive(Debug)]
32pub struct BackupLayoutGuard {
33 root: PathBuf,
34 _lock: JournalLock,
35}
36
37impl BackupLayoutGuard {
38 pub(crate) fn acquire(root: &Path) -> Result<Self, JournalLockError> {
39 let root = root.canonicalize()?;
40 let parent = root
41 .parent()
42 .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
43 let name = root
44 .file_name()
45 .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
46 let key = format!("{:x}", Sha256::digest(name.as_encoded_bytes()));
47 let lock = JournalLock::acquire(&parent.join(format!(".canic-backup-{key}")))?;
48 if !fs::symlink_metadata(&root)?.is_dir() {
50 return Err(io::Error::from(io::ErrorKind::NotADirectory).into());
51 }
52 Ok(Self { root, _lock: lock })
53 }
54
55 #[must_use]
57 pub fn root(&self) -> &Path {
58 &self.root
59 }
60
61 pub fn has_restore_references(&self) -> Result<bool, PersistenceError> {
63 Ok(!self.read_references()?.restores.is_empty())
64 }
65
66 pub(crate) fn retain_restore(
67 &self,
68 journal: &Path,
69 authority: &str,
70 ) -> Result<(), PersistenceError> {
71 let mut references = self.read_references()?;
72 let reference = RestoreReferenceRecord {
73 journal: journal.to_path_buf(),
74 authority: authority.to_owned(),
75 };
76 if let Some(existing) = references
77 .restores
78 .iter()
79 .find(|entry| entry.journal == journal)
80 {
81 if existing != &reference {
82 return Err(PersistenceError::RestoreReferenceConflict {
83 path: journal.display().to_string(),
84 });
85 }
86 fs::File::open(self.root.join(REFERENCES_FILE))?.sync_all()?;
88 fs::File::open(&self.root)?.sync_all()?;
89 return Ok(());
90 }
91 references.restores.push(reference);
92 write_json_durable(&self.root.join(REFERENCES_FILE), &references)
93 }
94
95 pub(crate) fn release_restore(
96 &self,
97 journal: &Path,
98 authority: &str,
99 ) -> Result<(), PersistenceError> {
100 let mut references = self.read_references()?;
101 let Some(index) = references
102 .restores
103 .iter()
104 .position(|entry| entry.journal == journal)
105 else {
106 return Ok(());
107 };
108 if references.restores[index].authority != authority {
109 return Err(PersistenceError::RestoreReferenceConflict {
110 path: journal.display().to_string(),
111 });
112 }
113 references.restores.remove(index);
114 write_json_durable(&self.root.join(REFERENCES_FILE), &references)
115 }
116
117 fn read_references(&self) -> Result<RestoreReferencesRecord, PersistenceError> {
118 let path = self.root.join(REFERENCES_FILE);
119 match fs::symlink_metadata(&path) {
120 Err(error) if error.kind() == io::ErrorKind::NotFound => {
121 return Ok(RestoreReferencesRecord {
122 version: 1,
123 restores: Vec::new(),
124 });
125 }
126 Err(error) => return Err(error.into()),
127 Ok(metadata) if !metadata.is_file() => {
128 return Err(PersistenceError::InvalidRestoreReferences {
129 path: path.display().to_string(),
130 });
131 }
132 Ok(_) => {}
133 }
134 let references: RestoreReferencesRecord = read_json(&path)?;
135 if references.version != 1 {
136 return Err(PersistenceError::InvalidRestoreReferences {
137 path: path.display().to_string(),
138 });
139 }
140 Ok(references)
141 }
142}
143
144#[derive(Debug)]
151pub struct BackupExecutionGuard {
152 pub(super) _lifetime: BackupLayoutGuard,
153 pub(super) _journal: JournalLock,
154}
155
156#[derive(Deserialize, Serialize)]
163#[serde(deny_unknown_fields)]
164struct RestoreReferencesRecord {
165 version: u16,
166 restores: Vec<RestoreReferenceRecord>,
167}
168
169#[derive(Deserialize, Eq, PartialEq, Serialize)]
176#[serde(deny_unknown_fields)]
177struct RestoreReferenceRecord {
178 journal: PathBuf,
179 authority: String,
180}