ic_backup/ops/persistence/layout_lifetime/
mod.rs1use crate::{
4 model::restore_references::{RestoreReferenceRecord, RestoreReferencesRecord},
5 ops::persistence::{
6 JournalLock, JournalLockError, PersistenceError, read_json, write_json_durable,
7 },
8};
9use sha2::{Digest, Sha256};
10use std::{
11 fs::{self, File},
12 io,
13 path::{Path, PathBuf},
14};
15
16const REFERENCES_FILE: &str = "restore-references.json";
17pub const MAX_RESTORE_REFERENCE_BYTES: u64 = 1024 * 1024;
19
20#[derive(Debug)]
25pub struct BackupLayoutGuard {
26 root: PathBuf,
27 directory: File,
28 _lock: JournalLock,
29}
30
31impl BackupLayoutGuard {
32 pub fn acquire(root: &Path) -> Result<Self, JournalLockError> {
40 let root = root.canonicalize()?;
41 let parent = root
42 .parent()
43 .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
44 let name = root
45 .file_name()
46 .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
47 let key = format!("{:x}", Sha256::digest(name.as_encoded_bytes()));
48 let lock = JournalLock::acquire(&parent.join(format!(".ic-backup-layout-{key}")))?;
49 let directory = open_directory(&root)?;
50 Ok(Self {
51 root,
52 directory,
53 _lock: lock,
54 })
55 }
56
57 #[must_use]
59 pub fn root(&self) -> &Path {
60 &self.root
61 }
62
63 pub fn restore_references(&self) -> Result<RestoreReferencesRecord, PersistenceError> {
68 self.check_root()?;
69 let path = self.root.join(REFERENCES_FILE);
70 match fs::symlink_metadata(&path) {
71 Err(error) if error.kind() == io::ErrorKind::NotFound => {
72 return Ok(RestoreReferencesRecord::empty());
73 }
74 Err(error) => return Err(error.into()),
75 Ok(metadata) if !metadata.is_file() => {
76 return Err(PersistenceError::InvalidRestoreReferences { path });
77 }
78 Ok(_) => {}
79 }
80 read_json(&path, MAX_RESTORE_REFERENCE_BYTES)
81 }
82
83 pub fn has_restore_references(&self) -> Result<bool, PersistenceError> {
88 Ok(!self.restore_references()?.is_empty())
89 }
90
91 pub fn retain_restore(
100 &self,
101 journal: &Path,
102 authority: &str,
103 ) -> Result<RestoreReferenceRecord, PersistenceError> {
104 self.retain_with(journal, authority, write_json_durable)
105 }
106
107 fn retain_with(
108 &self,
109 journal: &Path,
110 authority: &str,
111 write: impl FnOnce(&Path, &RestoreReferencesRecord) -> Result<(), PersistenceError>,
112 ) -> Result<RestoreReferenceRecord, PersistenceError> {
113 let mut references = self.restore_references()?;
114 let reference = RestoreReferenceRecord::new(journal_identity(journal)?, authority)?;
115 let path = self.root.join(REFERENCES_FILE);
116 if references.retain(reference.clone())? {
117 let bytes = serde_json::to_vec_pretty(&references)?;
118 if bytes.len() as u64 > MAX_RESTORE_REFERENCE_BYTES {
119 return Err(PersistenceError::RecordTooLarge {
120 limit: MAX_RESTORE_REFERENCE_BYTES,
121 });
122 }
123 write(&path, &references)?;
124 } else {
125 sync_reference(&path)?;
127 self.directory.sync_all()?;
128 }
129 Ok(reference)
130 }
131
132 fn check_root(&self) -> Result<(), PersistenceError> {
133 #[cfg(unix)]
134 {
135 use std::os::unix::fs::MetadataExt;
136 let current = fs::symlink_metadata(&self.root)?;
137 let held = self.directory.metadata()?;
138 if current.is_dir() && current.dev() == held.dev() && current.ino() == held.ino() {
139 return Ok(());
140 }
141 }
142 Err(PersistenceError::LayoutChanged {
143 path: self.root.clone(),
144 })
145 }
146}
147
148fn journal_identity(path: &Path) -> Result<PathBuf, PersistenceError> {
149 let name = path
150 .file_name()
151 .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
152 let parent = path
153 .parent()
154 .filter(|parent| !parent.as_os_str().is_empty())
155 .unwrap_or_else(|| Path::new("."));
156 let parent = parent.canonicalize()?;
157 if !parent.is_dir() {
158 return Err(io::Error::from(io::ErrorKind::NotADirectory).into());
159 }
160 let journal = parent.join(name);
161 match fs::symlink_metadata(&journal) {
162 Ok(metadata) if !metadata.is_file() => {
163 return Err(PersistenceError::InvalidRestoreReferences { path: journal });
164 }
165 Err(error) if error.kind() != io::ErrorKind::NotFound => return Err(error.into()),
166 _ => {}
167 }
168 Ok(journal)
169}
170
171fn open_directory(path: &Path) -> io::Result<File> {
172 #[cfg(unix)]
173 {
174 use rustix::fs::{Mode, OFlags, open};
175 let fd = open(
176 path,
177 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
178 Mode::empty(),
179 )
180 .map_err(|error| io::Error::from_raw_os_error(error.raw_os_error()))?;
181 Ok(File::from(fd))
182 }
183 #[cfg(not(unix))]
184 {
185 let _ = path;
186 Err(io::Error::from(io::ErrorKind::Unsupported))
187 }
188}
189
190fn sync_reference(path: &Path) -> io::Result<()> {
191 #[cfg(unix)]
192 {
193 use rustix::fs::{FileType, Mode, OFlags, fstat, open};
194 let fd = open(
195 path,
196 OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
197 Mode::empty(),
198 )
199 .map_err(|error| io::Error::from_raw_os_error(error.raw_os_error()))?;
200 let metadata =
201 fstat(&fd).map_err(|error| io::Error::from_raw_os_error(error.raw_os_error()))?;
202 if !FileType::from_raw_mode(metadata.st_mode).is_file() {
203 return Err(io::Error::from(io::ErrorKind::InvalidInput));
204 }
205 File::from(fd).sync_all()
206 }
207 #[cfg(not(unix))]
208 {
209 let _ = path;
210 Err(io::Error::from(io::ErrorKind::Unsupported))
211 }
212}
213
214#[cfg(all(test, unix))]
215mod tests;