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 ic_host_artifacts::artifact::Sha256Digest;
10#[cfg(unix)]
11use ic_host_fs::read::open_directory_no_follow;
12use std::{
13 fs::{self, File},
14 io,
15 path::{Path, PathBuf},
16};
17
18const REFERENCES_FILE: &str = "restore-references.json";
19pub const MAX_RESTORE_REFERENCE_BYTES: u64 = 1024 * 1024;
21
22#[derive(Debug)]
27pub struct BackupLayoutGuard {
28 root: PathBuf,
29 directory: File,
30 _lock: JournalLock,
31}
32
33impl BackupLayoutGuard {
34 pub fn acquire(root: &Path) -> Result<Self, JournalLockError> {
42 let root = root.canonicalize()?;
43 let parent = root
44 .parent()
45 .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
46 let name = root
47 .file_name()
48 .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
49 let key = Sha256Digest::compute(name.as_encoded_bytes()).to_string();
50 let lock = JournalLock::acquire(&parent.join(format!(".ic-backup-layout-{key}")))?;
51 let directory = open_directory_no_follow(&root)?;
52 Ok(Self {
53 root,
54 directory,
55 _lock: lock,
56 })
57 }
58
59 #[must_use]
61 pub fn root(&self) -> &Path {
62 &self.root
63 }
64
65 pub fn restore_references(&self) -> Result<RestoreReferencesRecord, PersistenceError> {
70 self.check_root()?;
71 let path = self.root.join(REFERENCES_FILE);
72 match fs::symlink_metadata(&path) {
73 Err(error) if error.kind() == io::ErrorKind::NotFound => {
74 return Ok(RestoreReferencesRecord::empty());
75 }
76 Err(error) => return Err(error.into()),
77 Ok(metadata) if !metadata.is_file() => {
78 return Err(PersistenceError::InvalidRestoreReferences { path });
79 }
80 Ok(_) => {}
81 }
82 read_json(&path, MAX_RESTORE_REFERENCE_BYTES)
83 }
84
85 pub fn has_restore_references(&self) -> Result<bool, PersistenceError> {
90 Ok(!self.restore_references()?.is_empty())
91 }
92
93 pub fn retain_restore(
102 &self,
103 journal: &Path,
104 authority: &str,
105 ) -> Result<RestoreReferenceRecord, PersistenceError> {
106 self.retain_with(journal, authority, write_json_durable)
107 }
108
109 fn retain_with(
110 &self,
111 journal: &Path,
112 authority: &str,
113 write: impl FnOnce(&Path, &RestoreReferencesRecord) -> Result<(), PersistenceError>,
114 ) -> Result<RestoreReferenceRecord, PersistenceError> {
115 let mut references = self.restore_references()?;
116 let reference = RestoreReferenceRecord::new(journal_identity(journal)?, authority)?;
117 let path = self.root.join(REFERENCES_FILE);
118 if references.retain(reference.clone())? {
119 super::json::check_json_size(&references, MAX_RESTORE_REFERENCE_BYTES)?;
120 write(&path, &references)?;
121 } else {
122 sync_reference(&path)?;
124 self.directory.sync_all()?;
125 }
126 Ok(reference)
127 }
128
129 pub(super) fn sync_references(&self) -> Result<(), PersistenceError> {
130 self.check_root()?;
131 let path = self.root.join(REFERENCES_FILE);
132 match fs::symlink_metadata(&path) {
133 Ok(_) => sync_reference(&path)?,
134 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
135 Err(error) => return Err(error.into()),
136 }
137 self.directory.sync_all()?;
138 Ok(())
139 }
140
141 pub(super) fn check_root(&self) -> Result<(), PersistenceError> {
142 match ic_host_fs::durable::admit_directory(&self.root, &self.directory) {
143 Ok(()) => Ok(()),
144 Err(error) if error.kind() == io::ErrorKind::InvalidInput => {
145 Err(PersistenceError::LayoutChanged {
146 path: self.root.clone(),
147 })
148 }
149 Err(error) => Err(error.into()),
150 }
151 }
152}
153
154#[cfg(not(unix))]
155fn open_directory_no_follow(_path: &Path) -> io::Result<File> {
156 Err(io::Error::from(io::ErrorKind::Unsupported))
157}
158
159pub(super) fn journal_identity(path: &Path) -> Result<PathBuf, PersistenceError> {
160 let name = path
161 .file_name()
162 .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
163 let parent = path
164 .parent()
165 .filter(|parent| !parent.as_os_str().is_empty())
166 .unwrap_or_else(|| Path::new("."));
167 let parent = parent.canonicalize()?;
168 if !parent.is_dir() {
169 return Err(io::Error::from(io::ErrorKind::NotADirectory).into());
170 }
171 let journal = parent.join(name);
172 match fs::symlink_metadata(&journal) {
173 Ok(metadata) if !metadata.is_file() => {
174 return Err(PersistenceError::InvalidRestoreReferences { path: journal });
175 }
176 Err(error) if error.kind() != io::ErrorKind::NotFound => return Err(error.into()),
177 _ => {}
178 }
179 Ok(journal)
180}
181
182fn sync_reference(path: &Path) -> io::Result<()> {
183 #[cfg(unix)]
184 {
185 use rustix::fs::{FileType, Mode, OFlags, fstat, open};
186 let fd = open(
187 path,
188 OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
189 Mode::empty(),
190 )
191 .map_err(io::Error::from)?;
192 let metadata = fstat(&fd).map_err(io::Error::from)?;
193 if !FileType::from_raw_mode(metadata.st_mode).is_file() {
194 return Err(io::Error::from(io::ErrorKind::InvalidInput));
195 }
196 File::from(fd).sync_all()
197 }
198 #[cfg(not(unix))]
199 {
200 let _ = path;
201 Err(io::Error::from(io::ErrorKind::Unsupported))
202 }
203}
204
205#[cfg(all(test, unix))]
206mod tests;