use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;
use super::{LeanError, LeanResult};
fn refuse(path: &Path, why: &str) -> LeanError {
LeanError::State(format!("refusing write to {}: {why} (containment)", path.display()))
}
pub(crate) fn check_parent(path: &Path) -> LeanResult<()> {
let Some(parent) = path.parent() else { return Ok(()) };
if let Ok(m) = std::fs::symlink_metadata(parent) {
if m.file_type().is_symlink() {
return Err(refuse(path, "parent directory is a symlink"));
}
}
Ok(())
}
pub(crate) fn write_via_tmp(
path: &Path,
tmp: &Path,
bytes: &[u8],
mode: Option<u32>,
) -> LeanResult<std::fs::Metadata> {
write_via_tmp_opts(path, tmp, bytes, mode, true, false)
}
pub(crate) fn write_via_tmp_fast(
path: &Path,
tmp: &Path,
bytes: &[u8],
mode: Option<u32>,
) -> LeanResult<std::fs::Metadata> {
write_via_tmp_opts(path, tmp, bytes, mode, false, true)
}
fn create_exclusive(tmp: &Path, mkdir_parent: bool) -> LeanResult<std::fs::File> {
let open = || OpenOptions::new().write(true).create_new(true).open(tmp);
let first = match open() {
Ok(f) => return Ok(f),
Err(e) => e,
};
let again = |why: &str, e: std::io::Error| {
refuse(tmp, &format!("temp file is not exclusively creatable after {why}: {e}"))
};
match first.kind() {
std::io::ErrorKind::AlreadyExists => {
if let Err(e) = std::fs::remove_file(tmp) {
if e.kind() != std::io::ErrorKind::NotFound {
return Err(refuse(tmp, &format!("stale temp file is not removable: {e}")));
}
}
open().map_err(|e| again("removing a leftover", e))
}
std::io::ErrorKind::NotFound if mkdir_parent => {
if let Some(parent) = tmp.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| refuse(tmp, &format!("mkdir for temp: {e}")))?;
}
open().map_err(|e| again("recreating its parent", e))
}
_ => Err(refuse(tmp, &format!("temp file is not exclusively creatable: {first}"))),
}
}
pub(crate) struct RangedTmp {
file: std::fs::File,
tmp: std::path::PathBuf,
path: std::path::PathBuf,
written: std::sync::atomic::AtomicU64,
expect: u64,
}
impl RangedTmp {
pub(crate) fn create(
path: &Path,
tmp: &Path,
size: u64,
mode: Option<u32>,
) -> LeanResult<RangedTmp> {
let f = create_exclusive(tmp, true)?;
#[cfg(unix)]
if let Some(mode) = mode {
use std::os::unix::fs::PermissionsExt;
f.set_permissions(std::fs::Permissions::from_mode(mode))
.map_err(|e| refuse(tmp, &format!("mode: {e}")))?;
}
#[cfg(not(unix))]
let _ = mode;
Ok(RangedTmp {
file: f,
tmp: tmp.to_path_buf(),
path: path.to_path_buf(),
written: std::sync::atomic::AtomicU64::new(0),
expect: size,
})
}
pub(crate) fn write_at(&self, offset: u64, bytes: &[u8]) -> LeanResult<()> {
#[cfg(unix)]
{
use std::os::unix::fs::FileExt;
self.file
.write_all_at(bytes, offset)
.map_err(|e| refuse(&self.tmp, &format!("write at {offset}: {e}")))?;
}
#[cfg(not(unix))]
{
use std::io::{Seek, SeekFrom, Write};
let mut f = &self.file;
f.seek(SeekFrom::Start(offset))
.map_err(|e| refuse(&self.tmp, &format!("seek to {offset}: {e}")))?;
f.write_all(bytes)
.map_err(|e| refuse(&self.tmp, &format!("write at {offset}: {e}")))?;
}
self.written
.fetch_add(bytes.len() as u64, std::sync::atomic::Ordering::Relaxed);
Ok(())
}
pub(crate) fn commit(self) -> LeanResult<()> {
let written = self.written.load(std::sync::atomic::Ordering::Relaxed);
if written != self.expect {
let _ = std::fs::remove_file(&self.tmp);
return Err(LeanError::State(format!(
"ranged materialisation of {} wrote {} of {} bytes — refusing to rename a \
file with a hole in it",
self.path.display(),
written,
self.expect
)));
}
drop(self.file);
std::fs::rename(&self.tmp, &self.path).map_err(|e| {
let _ = std::fs::remove_file(&self.tmp);
refuse(&self.path, &format!("rename from temp: {e}"))
})
}
}
pub(crate) fn sync_tree(dir: &Path) -> LeanResult<()> {
let d = std::fs::File::open(dir)
.map_err(|e| LeanError::State(format!("open {} to sync: {e}", dir.display())))?;
#[cfg(target_os = "linux")]
{
use std::os::fd::AsRawFd;
if unsafe { libc::syncfs(d.as_raw_fd()) } != 0 {
let e = std::io::Error::last_os_error();
return Err(LeanError::State(format!("syncfs {}: {e}", dir.display())));
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
{
d.sync_all()
.map_err(|e| LeanError::State(format!("fsync {}: {e}", dir.display())))
}
}
fn write_via_tmp_opts(
path: &Path,
tmp: &Path,
bytes: &[u8],
mode: Option<u32>,
durable: bool,
mkdir_parent: bool,
) -> LeanResult<std::fs::Metadata> {
let mut f = create_exclusive(tmp, mkdir_parent)?;
#[cfg(unix)]
if let Some(mode) = mode {
use std::os::unix::fs::PermissionsExt;
let _ = f.set_permissions(std::fs::Permissions::from_mode(mode & 0o7777));
}
#[cfg(not(unix))]
let _ = mode;
f.write_all(bytes)
.map_err(|e| LeanError::State(format!("write tmp for {}: {e}", path.display())))?;
if durable {
f.sync_all()
.map_err(|e| LeanError::State(format!("fsync tmp for {}: {e}", path.display())))?;
}
let meta = f
.metadata()
.map_err(|e| LeanError::State(format!("fstat tmp for {}: {e}", path.display())))?;
drop(f);
std::fs::rename(tmp, path)
.map_err(|e| LeanError::State(format!("rename into {}: {e}", path.display())))?;
if durable {
if let Some(parent) = path.parent() {
if let Ok(d) = std::fs::File::open(parent) {
let _ = d.sync_all();
}
}
}
Ok(meta)
}