atomic_file_install/
lib.rsuse std::{fs, io, path::Path};
use reflink_copy::reflink_or_copy;
use tempfile::{NamedTempFile, TempPath};
use tracing::{debug, warn};
#[cfg(unix)]
use std::os::unix::fs::symlink as symlink_file_inner;
#[cfg(windows)]
use std::os::windows::fs::symlink_file as symlink_file_inner;
fn parent(p: &Path) -> io::Result<&Path> {
p.parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("`{}` does not have a parent", p.display()),
)
})
}
fn copy_to_tempfile(src: &Path, dst: &Path) -> io::Result<NamedTempFile> {
let parent = parent(dst)?;
debug!("Creating named tempfile at '{}'", parent.display());
let tempfile = NamedTempFile::new_in(parent)?;
debug!(
"Copying from '{}' to '{}'",
src.display(),
tempfile.path().display()
);
fs::remove_file(tempfile.path())?;
reflink_or_copy(src, tempfile.path())?;
debug!("Retrieving permissions of '{}'", src.display());
let permissions = src.metadata()?.permissions();
debug!(
"Setting permissions of '{}' to '{permissions:#?}'",
tempfile.path().display()
);
tempfile.as_file().set_permissions(permissions)?;
Ok(tempfile)
}
pub fn atomic_install_noclobber(src: &Path, dst: &Path) -> io::Result<()> {
debug!(
"Attempting to rename from '{}' to '{}'.",
src.display(),
dst.display()
);
let tempfile = copy_to_tempfile(src, dst)?;
debug!(
"Persisting '{}' to '{}', fail if dst already exists",
tempfile.path().display(),
dst.display()
);
tempfile.persist_noclobber(dst)?;
Ok(())
}
pub fn atomic_install(src: &Path, dst: &Path) -> io::Result<()> {
debug!(
"Attempting to atomically rename from '{}' to '{}'",
src.display(),
dst.display()
);
if let Err(err) = fs::rename(src, dst) {
warn!("Attempting at atomic rename failed: {err}, fallback to other methods.");
#[cfg(windows)]
{
match win::replace_file(src, dst) {
Ok(()) => {
debug!("ReplaceFileW succeeded.");
return Ok(());
}
Err(err) => {
warn!("ReplaceFileW failed: {err}, fallback to using tempfile plus rename")
}
}
}
persist(copy_to_tempfile(src, dst)?.into_temp_path(), dst)?;
} else {
debug!("Attempting at atomically succeeded.");
}
Ok(())
}
pub fn atomic_symlink_file_noclobber(dest: &Path, link: &Path) -> io::Result<()> {
match symlink_file_inner(dest, link) {
Ok(_) => Ok(()),
#[cfg(windows)]
Err(_) => atomic_install_noclobber(dest, link),
#[cfg(not(windows))]
Err(err) => Err(err),
}
}
pub fn atomic_symlink_file(dest: &Path, link: &Path) -> io::Result<()> {
let parent = parent(link)?;
debug!("Creating tempPath at '{}'", parent.display());
let temp_path = NamedTempFile::new_in(parent)?.into_temp_path();
fs::remove_file(&temp_path)?;
debug!(
"Creating symlink '{}' to file '{}'",
temp_path.display(),
dest.display()
);
match symlink_file_inner(dest, &temp_path) {
Ok(_) => persist(temp_path, link),
#[cfg(windows)]
Err(_) => atomic_install(dest, link),
#[cfg(not(windows))]
Err(err) => Err(err),
}
}
fn persist(temp_path: TempPath, to: &Path) -> io::Result<()> {
debug!("Persisting '{}' to '{}'", temp_path.display(), to.display());
match temp_path.persist(to) {
Ok(()) => Ok(()),
#[cfg(windows)]
Err(tempfile::PathPersistError {
error,
path: temp_path,
}) => {
warn!(
"Failed to persist symlink '{}' to '{}': {error}, fallback to ReplaceFileW",
temp_path.display(),
to.display(),
);
win::replace_file(&temp_path, to).map_err(io::Error::from)
}
#[cfg(not(windows))]
Err(err) => Err(err.into()),
}
}
#[cfg(windows)]
mod win {
use std::{os::windows::ffi::OsStrExt, path::Path};
use windows::{
core::{Error, PCWSTR},
Win32::Storage::FileSystem::{ReplaceFileW, REPLACE_FILE_FLAGS},
};
pub(super) fn replace_file(src: &Path, dst: &Path) -> Result<(), Error> {
let mut src: Vec<_> = src.as_os_str().encode_wide().collect();
let mut dst: Vec<_> = dst.as_os_str().encode_wide().collect();
src.push(0);
dst.push(0);
unsafe {
ReplaceFileW(
PCWSTR::from_raw(dst.as_ptr()), PCWSTR::from_raw(src.as_ptr()), PCWSTR::null(), REPLACE_FILE_FLAGS(0), None, None, )
}
}
}