summaryrefslogtreecommitdiff
path: root/rola-utils/functions/src/copy_with_temp_rename
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-06-18 00:34:39 +0800
committer魏曹先生 <1992414357@qq.com>2026-06-18 00:34:39 +0800
commitdd9fa4b2104f7a19b4a364cf46b96b974c442cd1 (patch)
treeaf2488af4dadd9872a18c945ea0960cd5860d03d /rola-utils/functions/src/copy_with_temp_rename
parent7ea51858189338cbda1a21dc5724d1a8ce3aedb9 (diff)
refactor: split copy_with_temp_rename into sync and async modules
Diffstat (limited to 'rola-utils/functions/src/copy_with_temp_rename')
-rw-r--r--rola-utils/functions/src/copy_with_temp_rename/async.rs222
-rw-r--r--rola-utils/functions/src/copy_with_temp_rename/sync.rs209
2 files changed, 431 insertions, 0 deletions
diff --git a/rola-utils/functions/src/copy_with_temp_rename/async.rs b/rola-utils/functions/src/copy_with_temp_rename/async.rs
new file mode 100644
index 0000000..83275a1
--- /dev/null
+++ b/rola-utils/functions/src/copy_with_temp_rename/async.rs
@@ -0,0 +1,222 @@
+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<Path>,
+ dst: impl AsRef<Path>,
+) -> 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::new(
+ io::ErrorKind::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::new(
+ io::ErrorKind::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<u16> {
+ 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<u16>`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<PathBuf> {
+ 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::new(
+ io::ErrorKind::Other,
+ "exceeded max attempts to create temp file",
+ ))
+}
diff --git a/rola-utils/functions/src/copy_with_temp_rename/sync.rs b/rola-utils/functions/src/copy_with_temp_rename/sync.rs
new file mode 100644
index 0000000..cdbe3db
--- /dev/null
+++ b/rola-utils/functions/src/copy_with_temp_rename/sync.rs
@@ -0,0 +1,209 @@
+use std::borrow::Cow;
+use std::io;
+use std::path::{Path, PathBuf};
+
+/// Safely copies a file: first copies to a temporary file, then atomically replaces the destination.
+pub fn copy_with_temp_rename(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
+ internal_copy_with_temp_rename(src.as_ref(), dst.as_ref())
+}
+
+fn internal_copy_with_temp_rename(src: &Path, dst: &Path) -> io::Result<()> {
+ // Check if source file exists to avoid later failures
+ if !src.exists() {
+ 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 = std::fs::canonicalize(src);
+ if dst.exists() {
+ let dst_canonical = std::fs::canonicalize(dst);
+ 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::new(
+ io::ErrorKind::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::new(
+ io::ErrorKind::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 = src.metadata().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 = dst.metadata().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() {
+ std::fs::create_dir_all(parent)?;
+ }
+
+ // 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)?;
+
+ // Copy source file contents to the temporary file; clean up on failure
+ if let Err(e) = std::fs::copy(src, &temp_path) {
+ let _ = std::fs::remove_file(&temp_path);
+ return Err(e);
+ }
+
+ // Atomically replace the destination file; clean up temporary file on failure
+ if let Err(e) = atomic_rename(&temp_path, dst) {
+ let _ = std::fs::remove_file(&temp_path);
+ return Err(e);
+ }
+
+ // Copy source file permissions to the destination
+ let _ = std::fs::set_permissions(dst, src.metadata()?.permissions());
+
+ Ok(())
+}
+
+/// Atomically renames a path, replacing the destination if it exists.
+fn atomic_rename(src: &Path, dst: &Path) -> io::Result<()> {
+ #[cfg(unix)]
+ {
+ std::fs::rename(src, dst)
+ }
+
+ #[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<u16> {
+ 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<u16>`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(())
+ }
+ }
+}
+
+fn atomic_create_temp(dir: &Path, base: &str) -> io::Result<PathBuf> {
+ use std::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) {
+ Ok(_) => return Ok(path),
+ Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
+ Err(e) => return Err(e),
+ }
+ }
+
+ Err(io::Error::new(
+ io::ErrorKind::Other,
+ "exceeded max attempts to create temp file",
+ ))
+}