summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock22
-rw-r--r--Cargo.toml6
-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
5 files changed, 241 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock
index edb6412..9a5d3ef 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,6 +3,12 @@
version = 4
[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -23,6 +29,10 @@ dependencies = [
[[package]]
name = "rola-bucket"
version = "0.1.0"
+dependencies = [
+ "shared_functions",
+ "shared_macros",
+]
[[package]]
name = "rola-cli"
@@ -59,6 +69,9 @@ dependencies = [
[[package]]
name = "shared_functions"
version = "0.1.0"
+dependencies = [
+ "tokio",
+]
[[package]]
name = "shared_macros"
@@ -81,6 +94,15 @@ dependencies = [
]
[[package]]
+name = "tokio"
+version = "1.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+dependencies = [
+ "pin-project-lite",
+]
+
+[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 7f14aac..11ce5f3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,3 +24,9 @@ shared_functions = { path = "rola-utils/functions" }
shared_macros = { path = "rola-utils/macros" }
rola-bucket = { path = "rola-bucket" }
rola-draft = { path = "rola-draft" }
+
+[workspace.dependencies.tokio]
+version = "1.52.3"
+features = [
+ "fs"
+]
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::*;