use std::fs::{File, OpenOptions};
use std::path::Path;
use crate::MemoryError;
pub(crate) fn capture(message: impl Into<String>) -> MemoryError {
MemoryError::MigrationCapture(message.into())
}
fn not_a_regular_file(entity: &str) -> MemoryError {
MemoryError::NotARegularFile {
entity: entity.to_owned(),
}
}
pub(crate) fn validate_workspace(path: &Path, entity: &str) -> Result<(), MemoryError> {
let metadata = std::fs::symlink_metadata(path)
.map_err(|err| capture(format!("cannot inspect {entity} workspace: {err}")))?;
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return Err(capture(format!(
"{entity} workspace must be a real directory"
)));
}
Ok(())
}
pub(crate) fn validate_regular_file(path: &Path, entity: &str) -> Result<(), MemoryError> {
let metadata = std::fs::symlink_metadata(path)
.map_err(|err| capture(format!("cannot inspect {entity} file: {err}")))?;
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Err(capture(format!("{entity} path must be a regular file")));
}
Ok(())
}
pub(crate) fn open_regular_file(
path: &Path,
entity: &str,
options: OpenOptions,
) -> Result<File, MemoryError> {
let (file, metadata) = open_checked(path, entity, options)?;
#[cfg(not(unix))]
{
let _ = metadata;
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if metadata.nlink() > 1 {
return Err(not_a_regular_file(entity));
}
}
Ok(file)
}
pub(crate) fn open_regular_file_allow_hard_links(
path: &Path,
entity: &str,
options: OpenOptions,
) -> Result<(File, std::fs::Metadata), MemoryError> {
open_checked(path, entity, options)
}
fn open_checked(
path: &Path,
entity: &str,
mut options: OpenOptions,
) -> Result<(File, std::fs::Metadata), MemoryError> {
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
}
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
}
let file = options
.open(path)
.map_err(|err| open_error(&err, path, entity))?;
let metadata = file.metadata().map_err(|err| {
capture(format!(
"cannot inspect {entity} file {}: {err}",
path.display()
))
})?;
if !metadata.is_file() {
return Err(not_a_regular_file(entity));
}
Ok((file, metadata))
}
#[cfg(unix)]
fn open_error(err: &std::io::Error, path: &Path, entity: &str) -> MemoryError {
if err.raw_os_error() == Some(libc::ELOOP) {
return not_a_regular_file(entity);
}
let is_non_regular = std::fs::symlink_metadata(path).is_ok_and(|metadata| !metadata.is_file());
if is_non_regular {
not_a_regular_file(entity)
} else {
capture(format!(
"cannot open {entity} file {}: {err}",
path.display()
))
}
}
#[cfg(not(unix))]
fn open_error(err: &std::io::Error, path: &Path, entity: &str) -> MemoryError {
capture(format!(
"cannot open {entity} file {}: {err}",
path.display()
))
}
pub(crate) fn path_exists(path: &Path) -> Result<bool, MemoryError> {
match std::fs::symlink_metadata(path) {
Ok(_) => Ok(true),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(capture(format!(
"cannot inspect {}: {error}",
path.display()
))),
}
}
#[cfg(unix)]
pub(crate) fn promote(staging: &Path, final_path: &Path) -> std::io::Result<()> {
std::fs::rename(staging, final_path)
}
#[cfg(windows)]
pub(crate) fn promote(staging: &Path, final_path: &Path) -> std::io::Result<()> {
atomicwrites::replace_atomic(staging, final_path)
}
#[cfg(not(any(unix, windows)))]
pub(crate) fn promote(_staging: &Path, _final_path: &Path) -> std::io::Result<()> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"atomic file replacement unsupported",
))
}
#[cfg(unix)]
pub(crate) fn sync_directory(workspace: &Path) -> std::io::Result<()> {
std::fs::File::open(workspace)?.sync_all()
}
#[cfg(any(windows, not(any(unix, windows))))]
pub(crate) fn sync_directory(_workspace: &Path) -> std::io::Result<()> {
Ok(())
}
#[cfg(test)]
#[path = "atomic_file_tests.rs"]
mod atomic_file_tests;