use std::borrow::Cow; use std::path::{Path, PathBuf}; use tokio::io; /// Safely copies a file: first copies to a temporary file, then atomically replaces the destination. pub async fn copy_with_temp_rename_async( src: impl AsRef, dst: impl AsRef, ) -> io::Result<()> { internal_copy_with_temp_rename(src.as_ref(), dst.as_ref()).await } async fn internal_copy_with_temp_rename(src: &Path, dst: &Path) -> io::Result<()> { // Check if source file exists to avoid later failures if !tokio::fs::try_exists(src) .await .map_err(|e| io::Error::new(e.kind(), format!("failed to check source existence: {}", e)))? { return Err(io::Error::new( io::ErrorKind::NotFound, format!("source file does not exist: {}", src.display()), )); } // Canonicalize paths and compare; if same file, return early to avoid unnecessary copy // Only canonicalize destination if it exists; if it doesn't exist, skip the comparison let src_canonical = tokio::fs::canonicalize(src).await; let dst_exists = tokio::fs::try_exists(dst).await.unwrap_or(false); if dst_exists { let dst_canonical = tokio::fs::canonicalize(dst).await; match (src_canonical, dst_canonical) { (Ok(src_c), Ok(dst_c)) => { if src_c == dst_c { return Ok(()); } } (Err(e), _) | (_, Err(e)) => { return Err(io::Error::other(format!( "failed to canonicalize paths (src: {}, dst: {}): {}", src.display(), dst.display(), e ))); } } } else { // dst doesn't exist yet, so it cannot be the same file as src // If src canonicalization fails, propagate the error let _ = src_canonical.map_err(|e| { io::Error::other(format!( "failed to canonicalize source path (src: {}): {}", src.display(), e )) })?; } // Ensure both source and destination are regular files, preventing accidental operations on directories { let src_meta = tokio::fs::metadata(src).await.map_err(|e| { io::Error::new( e.kind(), format!("failed to get source metadata for {}: {}", src.display(), e), ) })?; if !src_meta.is_file() { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("source is not a regular file: {}", src.display()), )); } if dst_exists { let dst_meta = tokio::fs::metadata(dst).await.map_err(|e| { io::Error::new( e.kind(), format!( "failed to get destination metadata for {}: {}", dst.display(), e ), ) })?; if !dst_meta.is_file() { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!( "destination exists and is not a regular file: {}", dst.display() ), )); } } } // Ensure the parent directory of the destination exists, creating it if necessary if let Some(parent) = dst.parent() { tokio::fs::create_dir_all(parent).await?; } // Create a temporary file in the destination directory as an intermediate write target let temp_dir = dst.parent().unwrap_or_else(|| Path::new(".")); let base = src .file_name() .map(|f| f.to_string_lossy()) .unwrap_or_else(|| Cow::Borrowed("temp")); let temp_path = atomic_create_temp(temp_dir, &base).await?; // Copy source file contents to the temporary file; clean up on failure if let Err(e) = tokio::fs::copy(src, &temp_path).await { let _ = tokio::fs::remove_file(&temp_path).await; return Err(e); } // Atomically replace the destination file; clean up temporary file on failure if let Err(e) = atomic_rename(&temp_path, dst).await { let _ = tokio::fs::remove_file(&temp_path).await; return Err(e); } // Copy source file permissions to the destination let src_meta = tokio::fs::metadata(src).await?; let _ = tokio::fs::set_permissions(dst, src_meta.permissions()).await; Ok(()) } /// Atomically renames a path, replacing the destination if it exists. async fn atomic_rename(src: &Path, dst: &Path) -> io::Result<()> { #[cfg(unix)] { tokio::fs::rename(src, dst).await } #[cfg(not(unix))] { use std::ffi::OsStr; use std::os::windows::ffi::OsStrExt; // Convert OsStr to a null-terminated UTF-16 vector fn to_wide_null(os_str: &OsStr) -> Vec { os_str.encode_wide().chain(Some(0)).collect() } let src_wide = to_wide_null(src.as_os_str()); let dst_wide = to_wide_null(dst.as_os_str()); // SAFETY: `src_wide` and `dst_wide` are valid, owned `Vec`s, // the pointers returned by `as_ptr()` are valid for their lifetime and are null-terminated. // Declares the Windows API MoveFileExW for atomic renaming. unsafe extern "system" { /// MOVEFILE_REPLACE_EXISTING = 1 fn MoveFileExW( lpExistingFileName: *const u16, lpNewFileName: *const u16, dwFlags: u32, ) -> i32; } const MOVEFILE_REPLACE_EXISTING: u32 = 1; // SAFETY: // 1. `src_wide` and `dst_wide` are null-terminated UTF-16 strings, valid for their lifetime. // 2. `MoveFileExW` is a Windows system API declared via extern "system", // with correct calling convention and ABI. // 3. Return value is checked; errors are handled via `last_os_error()`. let ret = unsafe { MoveFileExW( src_wide.as_ptr(), dst_wide.as_ptr(), MOVEFILE_REPLACE_EXISTING, ) }; if ret == 0 { Err(io::Error::last_os_error()) } else { Ok(()) } } } async fn atomic_create_temp(dir: &Path, base: &str) -> io::Result { use tokio::fs::OpenOptions; // Generate temporary file names; first attempt without index, then incrementing to avoid conflicts let make_name = |i: usize| { let name = if i == 0 { format!(".temp_{}", base) } else { format!(".temp_{}_{}", base, i) }; dir.join(name) }; const MAX_ATTEMPTS: usize = 10000; // Loop to create a new file; create_new ensures the file does not already exist for i in 0..MAX_ATTEMPTS { let path = make_name(i); match OpenOptions::new() .create_new(true) .write(true) .open(&path) .await { Ok(_) => return Ok(path), Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => continue, Err(e) => return Err(e), } } Err(io::Error::other( "exceeded max attempts to create temp file", )) }