use std::io;
use std::path::Path;
use tempfile::NamedTempFile;
#[cfg(unix)]
fn sync_parent_dir(path: &Path) {
if let Ok(dir) = std::fs::File::open(crate::fs::paths::parent_or_cwd(path)) {
let _ = dir.sync_all();
}
}
#[cfg(not(unix))]
fn sync_parent_dir(_path: &Path) {}
pub(crate) fn finalize_file(tmp: NamedTempFile, final_path: &Path) -> io::Result<()> {
tmp.persist_noclobber(final_path).map_err(|e| e.error)?;
sync_parent_dir(final_path);
Ok(())
}
pub(crate) fn rename_no_clobber(from: &Path, to: &Path) -> io::Result<()> {
rename_no_clobber_impl(from, to)?;
sync_parent_dir(to);
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn rename_no_clobber_impl(from: &Path, to: &Path) -> io::Result<()> {
use rustix::fs::{CWD, RenameFlags, renameat_with};
renameat_with(CWD, from, CWD, to, RenameFlags::NOREPLACE).map_err(io::Error::from)
}
#[cfg(target_os = "windows")]
fn rename_no_clobber_impl(from: &Path, to: &Path) -> io::Result<()> {
match std::fs::symlink_metadata(to) {
Ok(_) => {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"Target already exists",
));
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => return Err(e),
}
std::fs::rename(from, to)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
fn rename_no_clobber_impl(_from: &Path, _to: &Path) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"Atomic rename is not supported on this target",
))
}
#[cfg(test)]
mod tests {
use std::fs;
use std::io::Write;
use super::*;
#[test]
fn finalize_file_refuses_to_overwrite() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let final_path = tmp_dir.path().join("out.txt");
fs::write(&final_path, "existing").unwrap();
let mut tmp = tempfile::Builder::new()
.tempfile_in(tmp_dir.path())
.unwrap();
tmp.write_all(b"new").unwrap();
let err = finalize_file(tmp, &final_path).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
assert_eq!(fs::read_to_string(&final_path).unwrap(), "existing");
}
#[test]
fn finalize_file_succeeds_when_target_missing() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let final_path = tmp_dir.path().join("out.txt");
let mut tmp = tempfile::Builder::new()
.tempfile_in(tmp_dir.path())
.unwrap();
tmp.write_all(b"payload").unwrap();
finalize_file(tmp, &final_path).unwrap();
assert_eq!(fs::read_to_string(&final_path).unwrap(), "payload");
}
#[test]
fn rename_no_clobber_refuses_to_overwrite_dir() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let from = tmp_dir.path().join("src");
let to = tmp_dir.path().join("dst");
fs::create_dir(&from).unwrap();
fs::write(from.join("inner.txt"), "new").unwrap();
fs::create_dir(&to).unwrap();
fs::write(to.join("existing.txt"), "existing").unwrap();
let err = rename_no_clobber(&from, &to).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
assert!(from.exists(), "source should not have been moved");
assert!(
to.join("existing.txt").exists(),
"destination should be untouched"
);
}
#[test]
fn rename_no_clobber_succeeds_when_target_missing_dir() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let from = tmp_dir.path().join("src");
let to = tmp_dir.path().join("dst");
fs::create_dir(&from).unwrap();
fs::write(from.join("payload.txt"), "hello").unwrap();
rename_no_clobber(&from, &to).unwrap();
assert!(!from.exists(), "source should have been moved");
assert!(to.is_dir(), "destination should exist as a directory");
assert_eq!(fs::read_to_string(to.join("payload.txt")).unwrap(), "hello",);
}
#[test]
fn rename_no_clobber_handles_regular_file() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let from = tmp_dir.path().join("staged.txt");
let to = tmp_dir.path().join("final.txt");
fs::write(&from, "payload").unwrap();
rename_no_clobber(&from, &to).unwrap();
assert!(!from.exists());
assert_eq!(fs::read_to_string(&to).unwrap(), "payload");
fs::write(&from, "second").unwrap();
let err = rename_no_clobber(&from, &to).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::AlreadyExists);
assert_eq!(fs::read_to_string(&to).unwrap(), "payload");
assert_eq!(fs::read_to_string(&from).unwrap(), "second");
}
#[test]
fn sync_parent_dir_swallows_missing_parent() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let phantom_parent = tmp_dir.path().join("does-not-exist");
let phantom_child = phantom_parent.join("child.txt");
sync_parent_dir(&phantom_child);
}
}