ic_backup/ops/persistence/layout_lifetime/
mod.rs1use crate::{
4 model::restore_references::{RestoreReferenceRecord, RestoreReferencesRecord},
5 ops::persistence::{JournalLock, JournalLockError, PersistenceError, read_json},
6};
7use ic_host_artifacts::artifact::Sha256Digest;
8#[cfg(unix)]
9use ic_host_fs::read::open_directory_no_follow;
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 = Sha256Digest::compute(name.as_encoded_bytes()).to_string();
48 let lock = JournalLock::acquire(&parent.join(format!(".ic-backup-layout-{key}")))?;
49 let directory = open_directory_no_follow(&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, super::json::write_encoded)
105 }
106
107 fn retain_with(
108 &self,
109 journal: &Path,
110 authority: &str,
111 write: impl FnOnce(&Path, &[u8]) -> Result<(), PersistenceError>,
112 ) -> Result<RestoreReferenceRecord, PersistenceError> {
113 let mut references = self.restore_references()?;
114 let reference = RestoreReferenceRecord::new(
115 super::journal_path::resolve_journal(
116 journal,
117 || io::Error::from(io::ErrorKind::InvalidInput).into(),
118 |path| PersistenceError::InvalidRestoreReferences { path },
119 )?,
120 authority,
121 )?;
122 let path = self.root.join(REFERENCES_FILE);
123 if references.retain(reference.clone())? {
124 let bytes = super::json::encode_json(&references, MAX_RESTORE_REFERENCE_BYTES)?;
125 write(&path, &bytes)?;
126 } else {
127 sync_reference(&path)?;
129 self.directory.sync_all()?;
130 }
131 Ok(reference)
132 }
133
134 pub(super) fn sync_references(&self) -> Result<(), PersistenceError> {
135 self.check_root()?;
136 let path = self.root.join(REFERENCES_FILE);
137 match fs::symlink_metadata(&path) {
138 Ok(_) => sync_reference(&path)?,
139 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
140 Err(error) => return Err(error.into()),
141 }
142 self.directory.sync_all()?;
143 Ok(())
144 }
145
146 pub(super) fn check_root(&self) -> Result<(), PersistenceError> {
147 match ic_host_fs::durable::admit_directory(&self.root, &self.directory) {
148 Ok(()) => Ok(()),
149 Err(error) if error.kind() == io::ErrorKind::InvalidInput => {
150 Err(PersistenceError::LayoutChanged {
151 path: self.root.clone(),
152 })
153 }
154 Err(error) => Err(error.into()),
155 }
156 }
157}
158
159#[cfg(not(unix))]
160fn open_directory_no_follow(_path: &Path) -> io::Result<File> {
161 Err(io::Error::from(io::ErrorKind::Unsupported))
162}
163
164fn sync_reference(path: &Path) -> io::Result<()> {
165 #[cfg(unix)]
166 {
167 ic_host_fs::read::open_regular_file_no_follow(path)
168 .map_err(|error| match error {
169 ic_host_artifacts::artifact::ArtifactError::NotRegularFile => {
170 io::Error::from(io::ErrorKind::InvalidInput)
171 }
172 error => error.into(),
173 })?
174 .sync_all()
175 }
176 #[cfg(not(unix))]
177 {
178 let _ = path;
179 Err(io::Error::from(io::ErrorKind::Unsupported))
180 }
181}
182
183#[cfg(all(test, unix))]
184mod tests;