use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use fs2::FileExt;
use sha2::{Digest, Sha256};
use tempfile::NamedTempFile;
use crate::transaction::{TransactionError, TransactionOptions};
#[derive(Debug, Clone, Copy)]
pub(crate) struct FileSystem {
private_mode: u32,
}
impl FileSystem {
pub(crate) const fn new(private_mode: u32) -> Self {
Self { private_mode }
}
pub(crate) fn prepare_roots(options: &TransactionOptions) -> Result<(), TransactionError> {
Self::create_dir(options.root())?;
Self::create_private_dir(options.state_root())?;
Self::create_private_dir(&options.state_root().join("locks"))?;
Self::create_private_dir(&options.state_root().join("backups"))
}
pub(crate) fn acquire_lock(options: &TransactionOptions) -> Result<File, TransactionError> {
let key = hex::encode(Sha256::digest(
options.root().as_os_str().as_encoded_bytes(),
));
let path = options
.state_root()
.join("locks")
.join(format!("{key}.lock"));
let file = OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(&path)
.map_err(|source| TransactionError::Io {
path: path.clone(),
source,
})?;
file.try_lock_exclusive()
.map_err(|_| TransactionError::LockBusy)?;
Ok(file)
}
pub(crate) fn atomic_write(
self,
root: &Path,
target: &Path,
content: &[u8],
mode: u32,
) -> Result<(), TransactionError> {
let relative = target
.strip_prefix(root)
.map_err(|_| TransactionError::Symlink {
path: target.to_path_buf(),
})?;
Self::ensure_path_safe(root, relative)?;
let parent = target.parent().ok_or_else(|| TransactionError::Symlink {
path: target.to_path_buf(),
})?;
Self::create_dir(parent)?;
let mut temporary =
NamedTempFile::new_in(parent).map_err(|source| TransactionError::Io {
path: parent.to_path_buf(),
source,
})?;
temporary
.write_all(content)
.and_then(|()| temporary.as_file().sync_all())
.map_err(|source| TransactionError::Io {
path: target.to_path_buf(),
source,
})?;
let output_mode = if mode == 0 {
self.private_mode
} else {
mode & 0o777
};
set_private_mode(temporary.as_file(), output_mode)?;
temporary
.persist(target)
.map_err(|error| TransactionError::Io {
path: target.to_path_buf(),
source: error.error,
})?;
sync_directory(parent)?;
Ok(())
}
pub(crate) fn ensure_path_safe(root: &Path, relative: &Path) -> Result<(), TransactionError> {
let mut current = root.to_path_buf();
for component in relative.components() {
current.push(component);
if let Ok(metadata) = fs::symlink_metadata(¤t) {
if metadata.file_type().is_symlink() {
return Err(TransactionError::Symlink { path: current });
}
}
}
Ok(())
}
pub(crate) fn create_dir(path: &Path) -> Result<(), TransactionError> {
fs::create_dir_all(path).map_err(|source| TransactionError::Io {
path: path.to_path_buf(),
source,
})
}
pub(crate) fn create_private_dir(path: &Path) -> Result<(), TransactionError> {
Self::create_dir(path)?;
set_directory_mode(path, 0o700)
}
pub(crate) fn read(path: &Path) -> Result<Vec<u8>, TransactionError> {
fs::read(path).map_err(|source| TransactionError::Io {
path: path.to_path_buf(),
source,
})
}
pub(crate) fn write(path: &Path, content: &[u8]) -> Result<(), TransactionError> {
fs::write(path, content).map_err(|source| TransactionError::Io {
path: path.to_path_buf(),
source,
})?;
set_path_mode(path, 0o600)
}
pub(crate) fn copy(source_path: &Path, target: &Path) -> Result<(), TransactionError> {
fs::copy(source_path, target)
.map(|_bytes| ())
.map_err(|source| TransactionError::Io {
path: source_path.to_path_buf(),
source,
})?;
set_path_mode(target, 0o600)
}
pub(crate) fn remove_file(path: &Path) -> Result<(), TransactionError> {
fs::remove_file(path).map_err(|source| TransactionError::Io {
path: path.to_path_buf(),
source,
})
}
pub(crate) fn remove_empty_parents(root: &Path, path: &Path) -> Result<(), TransactionError> {
let mut current = path.to_path_buf();
while current != root && current.starts_with(root) {
match fs::remove_dir(¤t) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::DirectoryNotEmpty => break,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(source) => {
return Err(TransactionError::Io {
path: current,
source,
});
}
}
let Some(parent) = current.parent() else {
break;
};
current = parent.to_path_buf();
}
Ok(())
}
}
#[cfg(unix)]
fn set_private_mode(file: &File, mode: u32) -> Result<(), TransactionError> {
use std::os::unix::fs::PermissionsExt;
file.set_permissions(fs::Permissions::from_mode(mode))
.map_err(|source| TransactionError::Io {
path: PathBuf::from("<staged-file>"),
source,
})
}
#[cfg(not(unix))]
fn set_private_mode(_file: &File, _mode: u32) -> Result<(), TransactionError> {
Ok(())
}
#[cfg(unix)]
fn sync_directory(path: &Path) -> Result<(), TransactionError> {
File::open(path)
.and_then(|directory| directory.sync_all())
.map_err(|source| TransactionError::Io {
path: path.to_path_buf(),
source,
})
}
#[cfg(not(unix))]
fn sync_directory(_path: &Path) -> Result<(), TransactionError> {
Ok(())
}
#[cfg(unix)]
fn set_directory_mode(path: &Path, mode: u32) -> Result<(), TransactionError> {
set_path_mode(path, mode)
}
#[cfg(not(unix))]
fn set_directory_mode(_path: &Path, _mode: u32) -> Result<(), TransactionError> {
Ok(())
}
#[cfg(unix)]
fn set_path_mode(path: &Path, mode: u32) -> Result<(), TransactionError> {
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(mode)).map_err(|source| {
TransactionError::Io {
path: path.to_path_buf(),
source,
}
})
}
#[cfg(not(unix))]
fn set_path_mode(_path: &Path, _mode: u32) -> Result<(), TransactionError> {
Ok(())
}