use anyhow::{Context, Result};
use base64::Engine;
use fs_err as fs;
use rand::{RngCore, rng}; use std::ffi::OsString;
use std::path::{Path, PathBuf};
#[cfg(unix)]
use rustix::fs::{Mode, OFlags, open};
#[cfg(unix)]
use std::os::unix::fs::symlink;
fn rand_suffix() -> String {
let mut b = [0u8; 6];
rng().fill_bytes(&mut b); base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b)
}
fn fsync_dir(dir: &Path) -> Result<()> {
#[cfg(unix)]
{
let f = open(dir, OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty())
.with_context(|| format!("open(dir) for fsync: {}", dir.display()))?;
rustix::fs::fdatasync(&f).context("fdatasync(dir)")?;
Ok(())
}
#[cfg(windows)]
{
let _ = dir;
Ok(())
}
}
pub(crate) fn is_exdev(err: &std::io::Error) -> bool {
match err.raw_os_error() {
#[cfg(unix)]
Some(n) => n == libc::EXDEV,
#[cfg(windows)]
Some(_n) => false,
None => false,
}
}
fn copy_file_fsync(src: &Path, dst: &Path) -> Result<()> {
let mut in_f = fs::File::open(src).with_context(|| format!("open(src): {}", src.display()))?;
let mut out_f =
fs::File::create(dst).with_context(|| format!("create(dst): {}", dst.display()))?;
std::io::copy(&mut in_f, &mut out_f).context("copy stream")?;
out_f.sync_all().context("fsync(dst)")?;
Ok(())
}
pub fn copy_recursively(src: &Path, dst: &Path) -> Result<()> {
let meta = fs::symlink_metadata(src)?;
if meta.is_dir() {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
copy_recursively(&entry.path(), &dst.join(entry.file_name()))?;
}
Ok(())
} else if meta.file_type().is_symlink() {
#[cfg(unix)]
{
let target = fs::read_link(src)?;
symlink(target, dst)?;
Ok(())
}
#[cfg(windows)]
{
let target = fs::read_link(src)?;
let real = if target.is_absolute() {
target
} else {
src.parent().unwrap_or(Path::new(".")).join(target)
};
copy_recursively(&real, dst)
}
} else {
copy_file_fsync(src, dst)
}
}
pub fn remove_recursively(p: &Path) -> Result<()> {
let meta = fs::symlink_metadata(p)?;
if meta.is_dir() && !meta.file_type().is_symlink() {
for entry in fs::read_dir(p)? {
let entry = entry?;
remove_recursively(&entry.path())?;
}
fs::remove_dir(p)?;
} else {
fs::remove_file(p)?;
}
Ok(())
}
pub fn safe_move_unique(src: &Path, dst_dir: &Path, basename: &OsString) -> Result<PathBuf> {
fs::create_dir_all(dst_dir).with_context(|| format!("create_dir_all {}", dst_dir.display()))?;
let ts = chrono::Local::now().format("%Y%m%dT%H%M%S");
let unique = format!(
"{}__{}__{}",
ts,
rand_suffix(),
Path::new(basename).to_string_lossy()
);
let dst = dst_dir.join(unique);
match fs::rename(src, &dst) {
Ok(()) => {
fsync_dir(dst_dir)?;
Ok(dst)
}
Err(e) if is_exdev(&e) => {
let tmp = dst.with_extension("copying");
copy_recursively(src, &tmp)?;
fsync_dir(tmp.parent().unwrap_or(dst_dir))?;
fs::rename(&tmp, &dst).context("swap tmp->dst")?;
fsync_dir(dst_dir)?;
remove_recursively(src)?;
Ok(dst)
}
Err(e) => Err(e).with_context(|| format!("rename {} -> {}", src.display(), dst.display())),
}
}