use std::io;
use std::path::Path;
pub fn atomic_replace(tmp_path: &Path, final_path: &Path) -> io::Result<()> {
{
let f = std::fs::File::open(tmp_path)?;
f.sync_all()?;
}
match std::fs::rename(tmp_path, final_path) {
Ok(()) => {}
Err(e) if is_cross_device(&e) => {
let dest_tmp = cross_device_tmp_path(final_path);
std::fs::copy(tmp_path, &dest_tmp)?;
match std::fs::File::open(&dest_tmp) {
Ok(f) => {
if let Err(fsync_err) = f.sync_all() {
tracing::debug!(
error = %fsync_err,
path = %dest_tmp.display(),
"fsync of cross-device dest tmp failed (non-fatal)"
);
}
}
Err(open_err) => {
tracing::debug!(
error = %open_err,
path = %dest_tmp.display(),
"could not reopen cross-device dest tmp for fsync"
);
}
}
if let Err(rn_err) = std::fs::rename(&dest_tmp, final_path) {
let _ = std::fs::remove_file(&dest_tmp);
let _ = std::fs::remove_file(tmp_path);
return Err(rn_err);
}
let _ = std::fs::remove_file(tmp_path);
}
Err(e) => return Err(e),
}
if let Some(parent) = final_path.parent() {
match std::fs::File::open(parent) {
Ok(dir) => {
if let Err(e) = dir.sync_all() {
tracing::debug!(
error = %e,
parent = %parent.display(),
"parent dir fsync failed after atomic_replace (non-fatal)"
);
}
}
Err(e) => {
tracing::debug!(
error = %e,
parent = %parent.display(),
"could not open parent dir for fsync after atomic_replace (non-fatal)"
);
}
}
}
Ok(())
}
fn is_cross_device(e: &io::Error) -> bool {
if matches!(e.kind(), io::ErrorKind::CrossesDevices) {
return true;
}
#[cfg(unix)]
{
if let Some(code) = e.raw_os_error() {
return code == libc::EXDEV;
}
}
false
}
fn cross_device_tmp_path(final_path: &Path) -> std::path::PathBuf {
let dir = final_path.parent().unwrap_or(Path::new("."));
let name = final_path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "file".to_string());
let suffix = crate::temp_suffix();
let pid = std::process::id();
dir.join(format!(".{}.xdev.{}.{:016x}.tmp", name, pid, suffix))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn atomic_replace_same_fs_moves_bytes_to_final_path() {
let dir = tempfile::TempDir::new().unwrap();
let tmp = dir.path().join(".config.tmp");
let final_path = dir.path().join("config.toml");
let payload = b"hello = 1\n";
std::fs::write(&tmp, payload).unwrap();
atomic_replace(&tmp, &final_path).unwrap();
assert!(!tmp.exists(), "temp file should be gone after rename");
assert!(final_path.exists(), "final file should exist");
let got = std::fs::read(&final_path).unwrap();
assert_eq!(got, payload);
}
#[test]
fn atomic_replace_overwrites_existing_final_path() {
let dir = tempfile::TempDir::new().unwrap();
let tmp = dir.path().join(".config.tmp");
let final_path = dir.path().join("config.toml");
std::fs::write(&final_path, b"old contents").unwrap();
std::fs::write(&tmp, b"new contents").unwrap();
atomic_replace(&tmp, &final_path).unwrap();
let got = std::fs::read(&final_path).unwrap();
assert_eq!(got, b"new contents");
}
#[test]
fn atomic_replace_missing_tmp_returns_error() {
let dir = tempfile::TempDir::new().unwrap();
let tmp = dir.path().join("does_not_exist.tmp");
let final_path = dir.path().join("config.toml");
let err = atomic_replace(&tmp, &final_path).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
assert!(!final_path.exists());
}
#[test]
fn atomic_replace_preserves_written_bytes_over_flush() {
let dir = tempfile::TempDir::new().unwrap();
let tmp = dir.path().join(".ids.tmp");
let final_path = dir.path().join("ids");
{
let f = std::fs::File::create(&tmp).unwrap();
let mut w = std::io::BufWriter::new(f);
w.write_all(b"0:a\n1:b\n").unwrap();
w.flush().unwrap();
}
atomic_replace(&tmp, &final_path).unwrap();
let got = std::fs::read_to_string(&final_path).unwrap();
assert_eq!(got, "0:a\n1:b\n");
}
}