use crate::{
model::restore_references::{RestoreReferenceRecord, RestoreReferencesRecord},
ops::persistence::{
JournalLock, JournalLockError, PersistenceError, read_json, write_json_durable,
},
};
use ic_host_artifacts::artifact::Sha256Digest;
use std::{
fs::{self, File},
io,
path::{Path, PathBuf},
};
const REFERENCES_FILE: &str = "restore-references.json";
pub const MAX_RESTORE_REFERENCE_BYTES: u64 = 1024 * 1024;
#[derive(Debug)]
pub struct BackupLayoutGuard {
root: PathBuf,
directory: File,
_lock: JournalLock,
}
impl BackupLayoutGuard {
pub fn acquire(root: &Path) -> Result<Self, JournalLockError> {
let root = root.canonicalize()?;
let parent = root
.parent()
.ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
let name = root
.file_name()
.ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
let key = Sha256Digest::compute(name.as_encoded_bytes()).to_string();
let lock = JournalLock::acquire(&parent.join(format!(".ic-backup-layout-{key}")))?;
let directory = open_directory(&root)?;
Ok(Self {
root,
directory,
_lock: lock,
})
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
pub fn restore_references(&self) -> Result<RestoreReferencesRecord, PersistenceError> {
self.check_root()?;
let path = self.root.join(REFERENCES_FILE);
match fs::symlink_metadata(&path) {
Err(error) if error.kind() == io::ErrorKind::NotFound => {
return Ok(RestoreReferencesRecord::empty());
}
Err(error) => return Err(error.into()),
Ok(metadata) if !metadata.is_file() => {
return Err(PersistenceError::InvalidRestoreReferences { path });
}
Ok(_) => {}
}
read_json(&path, MAX_RESTORE_REFERENCE_BYTES)
}
pub fn has_restore_references(&self) -> Result<bool, PersistenceError> {
Ok(!self.restore_references()?.is_empty())
}
pub fn retain_restore(
&self,
journal: &Path,
authority: &str,
) -> Result<RestoreReferenceRecord, PersistenceError> {
self.retain_with(journal, authority, write_json_durable)
}
fn retain_with(
&self,
journal: &Path,
authority: &str,
write: impl FnOnce(&Path, &RestoreReferencesRecord) -> Result<(), PersistenceError>,
) -> Result<RestoreReferenceRecord, PersistenceError> {
let mut references = self.restore_references()?;
let reference = RestoreReferenceRecord::new(journal_identity(journal)?, authority)?;
let path = self.root.join(REFERENCES_FILE);
if references.retain(reference.clone())? {
super::json::check_json_size(&references, MAX_RESTORE_REFERENCE_BYTES)?;
write(&path, &references)?;
} else {
sync_reference(&path)?;
self.directory.sync_all()?;
}
Ok(reference)
}
pub(super) fn check_root(&self) -> Result<(), PersistenceError> {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let current = fs::symlink_metadata(&self.root)?;
let held = self.directory.metadata()?;
if current.is_dir() && current.dev() == held.dev() && current.ino() == held.ino() {
return Ok(());
}
}
Err(PersistenceError::LayoutChanged {
path: self.root.clone(),
})
}
}
fn journal_identity(path: &Path) -> Result<PathBuf, PersistenceError> {
let name = path
.file_name()
.ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
let parent = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let parent = parent.canonicalize()?;
if !parent.is_dir() {
return Err(io::Error::from(io::ErrorKind::NotADirectory).into());
}
let journal = parent.join(name);
match fs::symlink_metadata(&journal) {
Ok(metadata) if !metadata.is_file() => {
return Err(PersistenceError::InvalidRestoreReferences { path: journal });
}
Err(error) if error.kind() != io::ErrorKind::NotFound => return Err(error.into()),
_ => {}
}
Ok(journal)
}
fn open_directory(path: &Path) -> io::Result<File> {
#[cfg(unix)]
{
use rustix::fs::{Mode, OFlags, open};
let fd = open(
path,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
)
.map_err(io::Error::from)?;
Ok(File::from(fd))
}
#[cfg(not(unix))]
{
let _ = path;
Err(io::Error::from(io::ErrorKind::Unsupported))
}
}
fn sync_reference(path: &Path) -> io::Result<()> {
#[cfg(unix)]
{
use rustix::fs::{FileType, Mode, OFlags, fstat, open};
let fd = open(
path,
OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
Mode::empty(),
)
.map_err(io::Error::from)?;
let metadata = fstat(&fd).map_err(io::Error::from)?;
if !FileType::from_raw_mode(metadata.st_mode).is_file() {
return Err(io::Error::from(io::ErrorKind::InvalidInput));
}
File::from(fd).sync_all()
}
#[cfg(not(unix))]
{
let _ = path;
Err(io::Error::from(io::ErrorKind::Unsupported))
}
}
#[cfg(all(test, unix))]
mod tests;