summaryrefslogtreecommitdiff
path: root/rola-utils
diff options
context:
space:
mode:
Diffstat (limited to 'rola-utils')
-rw-r--r--rola-utils/functions/Cargo.toml1
-rw-r--r--rola-utils/functions/src/copy_with_temp_rename.rs209
-rw-r--r--rola-utils/functions/src/lib.rs3
3 files changed, 213 insertions, 0 deletions
diff --git a/rola-utils/functions/Cargo.toml b/rola-utils/functions/Cargo.toml
index dd482c4..564dc54 100644
--- a/rola-utils/functions/Cargo.toml
+++ b/rola-utils/functions/Cargo.toml
@@ -6,3 +6,4 @@ authors.workspace = true
license.workspace = true
[dependencies]
+tokio.workspace = true
diff --git a/rola-utils/functions/src/copy_with_temp_rename.rs b/rola-utils/functions/src/copy_with_temp_rename.rs
new file mode 100644
index 0000000..cdbe3db
--- /dev/null
+++ b/rola-utils/functions/src/copy_with_temp_rename.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",
+ ))
+}
diff --git a/rola-utils/functions/src/lib.rs b/rola-utils/functions/src/lib.rs
index ff2ee7e..40f2ca9 100644
--- a/rola-utils/functions/src/lib.rs
+++ b/rola-utils/functions/src/lib.rs
@@ -1,2 +1,5 @@
mod levenshtein_distance;
pub use levenshtein_distance::*;
+
+mod copy_with_temp_rename;
+pub use copy_with_temp_rename::*;