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