#[cfg(unix)]
extern crate rustix;
extern crate tempfile;
use std::convert::AsRef;
use std::error::Error as ErrorTrait;
use std::fmt;
use std::fs;
use std::io;
use std::path;
pub use OverwriteBehavior::{AllowOverwrite, DisallowOverwrite};
#[derive(Clone, Copy)]
pub enum OverwriteBehavior {
AllowOverwrite,
DisallowOverwrite,
}
#[derive(Debug)]
pub enum Error<E> {
Internal(io::Error),
User(E),
}
impl From<Error<io::Error>> for io::Error {
fn from(e: Error<io::Error>) -> Self {
match e {
Error::Internal(x) => x,
Error::User(x) => x,
}
}
}
impl<E: fmt::Display> fmt::Display for Error<E> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Error::Internal(ref e) => e.fmt(f),
Error::User(ref e) => e.fmt(f),
}
}
}
impl<E: ErrorTrait> ErrorTrait for Error<E> {
fn cause(&self) -> Option<&dyn ErrorTrait> {
match *self {
Error::Internal(ref e) => Some(e),
Error::User(ref e) => Some(e),
}
}
}
fn safe_parent(p: &path::Path) -> Option<&path::Path> {
match p.parent() {
None => None,
Some(x) if x.as_os_str().is_empty() => Some(path::Path::new(".")),
x => x,
}
}
pub struct AtomicFile {
path: path::PathBuf,
overwrite: OverwriteBehavior,
tmpdir: path::PathBuf,
}
impl AtomicFile {
pub fn new<P>(path: P, overwrite: OverwriteBehavior) -> Self
where
P: AsRef<path::Path>,
{
let p = path.as_ref();
AtomicFile::new_with_tmpdir(
p,
overwrite,
safe_parent(p).unwrap_or_else(|| path::Path::new(".")),
)
}
pub fn new_with_tmpdir<P, Q>(path: P, overwrite: OverwriteBehavior, tmpdir: Q) -> Self
where
P: AsRef<path::Path>,
Q: AsRef<path::Path>,
{
AtomicFile {
path: path.as_ref().to_path_buf(),
overwrite,
tmpdir: tmpdir.as_ref().to_path_buf(),
}
}
fn commit(&self, tmppath: &path::Path) -> io::Result<()> {
match self.overwrite {
AllowOverwrite => replace_atomic(tmppath, self.path()),
DisallowOverwrite => move_atomic(tmppath, self.path()),
}
}
pub fn path(&self) -> &path::Path {
&self.path
}
pub fn write<T, E, F>(&self, f: F) -> Result<T, Error<E>>
where
F: FnOnce(&mut fs::File) -> Result<T, E>,
{
let mut options = fs::OpenOptions::new();
options.write(true).create(true).truncate(true);
self.write_with_options(f, options)
}
pub fn write_with_options<T, E, F>(&self, f: F, options: fs::OpenOptions) -> Result<T, Error<E>>
where
F: FnOnce(&mut fs::File) -> Result<T, E>,
{
let tmpdir = tempfile::Builder::new()
.prefix(".atomicwrite")
.tempdir_in(&self.tmpdir)
.map_err(Error::Internal)?;
let tmppath = tmpdir.path().join("tmpfile.tmp");
let rv = {
let mut tmpfile = options.open(&tmppath).map_err(Error::Internal)?;
let r = f(&mut tmpfile).map_err(Error::User)?;
tmpfile.sync_all().map_err(Error::Internal)?;
r
};
self.commit(&tmppath).map_err(Error::Internal)?;
Ok(rv)
}
}
#[cfg(unix)]
mod imp {
use super::safe_parent;
use rustix::fs::AtFlags;
use std::{fs, io, path};
pub fn replace_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
let src_parent_path = safe_parent(src).unwrap();
let dst_parent_path = safe_parent(dst).unwrap();
let src_child_path = src.file_name().unwrap();
let dst_child_path = dst.file_name().unwrap();
let src_parent = fs::File::open(src_parent_path)?;
let dst_parent;
let dst_parent = if src_parent_path == dst_parent_path {
&src_parent
} else {
dst_parent = fs::File::open(dst_parent_path)?;
&dst_parent
};
rustix::fs::renameat(&src_parent, src_child_path, dst_parent, dst_child_path)?;
src_parent.sync_all()?;
if src_parent_path != dst_parent_path {
dst_parent.sync_all()?;
}
Ok(())
}
pub fn move_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
let src_parent_path = safe_parent(src).unwrap();
let dst_parent_path = safe_parent(dst).unwrap();
let src_child_path = src.file_name().unwrap();
let dst_child_path = dst.file_name().unwrap();
let src_parent = fs::File::open(src_parent_path)?;
let dst_parent;
let dst_parent = if src_parent_path == dst_parent_path {
&src_parent
} else {
dst_parent = fs::File::open(dst_parent_path)?;
&dst_parent
};
#[cfg(any(target_os = "android", target_os = "linux"))]
{
use rustix::fs::RenameFlags;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering::Relaxed;
static NO_RENAMEAT2: AtomicBool = AtomicBool::new(false);
if !NO_RENAMEAT2.load(Relaxed) {
match rustix::fs::renameat_with(
&src_parent,
src_child_path,
dst_parent,
dst_child_path,
RenameFlags::NOREPLACE,
) {
Ok(()) => {
src_parent.sync_all()?;
if src_parent_path != dst_parent_path {
dst_parent.sync_all()?;
}
return Ok(());
}
Err(rustix::io::Errno::NOSYS) => {
NO_RENAMEAT2.store(true, Relaxed);
}
Err(e) => return Err(e.into()),
}
}
}
rustix::fs::linkat(
&src_parent,
src_child_path,
dst_parent,
dst_child_path,
AtFlags::empty(),
)?;
rustix::fs::unlinkat(&src_parent, src_child_path, AtFlags::empty())?;
src_parent.sync_all()?;
if src_parent_path != dst_parent_path {
dst_parent.sync_all()?;
}
Ok(())
}
}
#[cfg(windows)]
mod imp {
extern crate windows_sys;
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use std::{io, path};
macro_rules! call {
($e: expr) => {
if $e != 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
};
}
fn path_to_windows_str<T: AsRef<OsStr>>(x: T) -> Vec<u16> {
x.as_ref().encode_wide().chain(Some(0)).collect()
}
pub fn replace_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
call!(unsafe {
windows_sys::Win32::Storage::FileSystem::MoveFileExW(
path_to_windows_str(src).as_ptr(),
path_to_windows_str(dst).as_ptr(),
windows_sys::Win32::Storage::FileSystem::MOVEFILE_WRITE_THROUGH
| windows_sys::Win32::Storage::FileSystem::MOVEFILE_REPLACE_EXISTING,
)
})
}
pub fn move_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
call!(unsafe {
windows_sys::Win32::Storage::FileSystem::MoveFileExW(
path_to_windows_str(src).as_ptr(),
path_to_windows_str(dst).as_ptr(),
windows_sys::Win32::Storage::FileSystem::MOVEFILE_WRITE_THROUGH,
)
})
}
}
pub fn replace_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
imp::replace_atomic(src, dst)
}
pub fn move_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
imp::move_atomic(src, dst)
}