diff --git a/Cargo.lock b/Cargo.lock index ae4e843e..c05245f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2474,6 +2474,12 @@ dependencies = [ "winsafe", ] +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + [[package]] name = "winapi" version = "0.3.9" @@ -2964,6 +2970,8 @@ dependencies = [ "serde", "shell-words", "tokio", + "widestring", + "windows-sys 0.52.0", ] [[package]] diff --git a/yazi-core/src/manager/commands/bulk_rename.rs b/yazi-core/src/manager/commands/bulk_rename.rs index df1bb47a..7f98c7ad 100644 --- a/yazi-core/src/manager/commands/bulk_rename.rs +++ b/yazi-core/src/manager/commands/bulk_rename.rs @@ -6,7 +6,7 @@ use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}}; use yazi_config::{OPEN, PREVIEW}; use yazi_dds::Pubsub; use yazi_proxy::{AppProxy, TasksProxy, HIDER, WATCHER}; -use yazi_shared::{fs::{are_pathss_equal, max_common_root, maybe_exists, File, FilesOp, Url}, terminal_clear}; +use yazi_shared::{fs::{max_common_root, maybe_exists, File, FilesOp, Url}, terminal_clear}; use crate::manager::Manager; @@ -84,14 +84,32 @@ impl Manager { for (o, n) in todo { let (old, new) = (root.join(&o), root.join(&n)); - if maybe_exists(&new).await && !are_pathss_equal(&old, &new).await { + let overwrite_safe; + #[cfg(windows)] + { + overwrite_safe = yazi_shared::fs::rename_without_overwriting(&old, &new).await.is_ok(); + } + #[cfg(unix)] + { + overwrite_safe = yazi_shared::fs::are_paths_equal(&old, &new).await; + } + if maybe_exists(&new).await && !overwrite_safe { failed.push((o, n, anyhow!("Destination already exists"))); - } else if let Err(e) = fs::rename(&old, &new).await { - failed.push((o, n, e.into())); - } else if let Ok(f) = File::from(new.into()).await { - succeeded.insert(Url::from(old), f); } else { - failed.push((o, n, anyhow!("Failed to retrieve file info"))); + #[cfg(unix)] + if let Err(e) = fs::rename(&old, &new).await { + failed.push((o, n, e.into())); + } else if let Ok(f) = File::from(new.into()).await { + succeeded.insert(Url::from(old), f); + } else { + failed.push((o, n, anyhow!("Failed to retrieve file info"))); + } + #[cfg(windows)] + if let Ok(f) = File::from(new.into()).await { + succeeded.insert(Url::from(old), f); + } else { + failed.push((o, n, anyhow!("Failed to retrieve file info"))); + } } } diff --git a/yazi-core/src/manager/commands/rename.rs b/yazi-core/src/manager/commands/rename.rs index d36a84f9..0241af90 100644 --- a/yazi-core/src/manager/commands/rename.rs +++ b/yazi-core/src/manager/commands/rename.rs @@ -5,7 +5,7 @@ use tokio::fs; use yazi_config::popup::InputCfg; use yazi_dds::Pubsub; use yazi_proxy::{InputProxy, TabProxy, WATCHER}; -use yazi_shared::{event::Cmd, fs::{are_pathss_equal, maybe_exists, ok_or_not_found, symlink_realpath, File, FilesOp, Url}}; +use yazi_shared::{event::Cmd, fs::{maybe_exists, ok_or_not_found, symlink_realpath, File, FilesOp, Url}}; use crate::manager::Manager; @@ -62,36 +62,48 @@ impl Manager { } let new = hovered.parent().unwrap().join(name); - if opt.force || !maybe_exists(&new).await || are_pathss_equal(&hovered, &new).await { - Self::rename_do(tab, hovered, Url::from(new)).await.ok(); + let overwrite_safe; + #[cfg(windows)] + { + overwrite_safe = yazi_shared::fs::rename_without_overwriting(&hovered, &new).await.is_ok(); + } + #[cfg(unix)] + { + overwrite_safe = yazi_shared::fs::are_paths_equal(&hovered, &new).await; + } + if opt.force || !maybe_exists(&new).await || overwrite_safe { + Self::rename_do(tab, hovered, Url::from(new), overwrite_safe).await.ok(); return; } let mut result = InputProxy::show(InputCfg::overwrite()); if let Some(Ok(choice)) = result.recv().await { if choice == "y" || choice == "Y" { - Self::rename_do(tab, hovered, Url::from(new)).await.ok(); + Self::rename_do(tab, hovered, Url::from(new), false).await.ok(); } }; }); } - async fn rename_do(tab: usize, old: Url, new: Url) -> Result<()> { + async fn rename_do(tab: usize, old: Url, new: Url, overwrite_safe: bool) -> Result<()> { let Some(p_old) = old.parent_url() else { return Ok(()) }; let Some(p_new) = new.parent_url() else { return Ok(()) }; let _permit = WATCHER.acquire().await.unwrap(); - let are_different = !are_pathss_equal(&old, &new).await; - let overwritten = symlink_realpath(&new).await; - fs::rename(&old, &new).await?; + if overwrite_safe { + if !cfg!(windows) { + fs::rename(&old, &new).await?; + } + } else { + let overwritten = symlink_realpath(&new).await; + fs::rename(&old, &new).await?; - if are_different { if let Ok(p) = overwritten { ok_or_not_found(fs::rename(&p, &new).await)?; FilesOp::Deleting(p_new.clone(), vec![Url::from(p)]).emit(); } - Pubsub::pub_from_rename(tab, &old, &new); } + Pubsub::pub_from_rename(tab, &old, &new); let file = File::from(new.clone()).await?; FilesOp::Deleting(p_old, vec![old]).emit(); diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index 414bba0d..ba31f15c 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -22,6 +22,8 @@ regex = "1.10.5" serde = { version = "1.0.203", features = [ "derive" ] } shell-words = "1.1.0" tokio = { version = "1.38.0", features = [ "full" ] } +widestring = "1.1.0" +windows-sys = { version = "0.52.0", features = ["Win32_Foundation"] } [target."cfg(unix)".dependencies] libc = "0.2.155" diff --git a/yazi-shared/src/fs/fns.rs b/yazi-shared/src/fs/fns.rs index 20f1c9a4..99c8dcc3 100644 --- a/yazi-shared/src/fs/fns.rs +++ b/yazi-shared/src/fs/fns.rs @@ -24,8 +24,8 @@ pub fn ok_or_not_found(result: io::Result<()>) -> io::Result<()> { } #[inline] -pub async fn are_pathss_equal(old: impl AsRef, new: impl AsRef) -> bool { - #[cfg(unix)] +#[cfg(unix)] +pub async fn are_paths_equal(old: impl AsRef, new: impl AsRef) -> bool { if let (Ok(canonical_old), Ok(canonical_new)) = (fs::canonicalize(&old).await, fs::canonicalize(&new).await) { @@ -40,10 +40,22 @@ pub async fn are_pathss_equal(old: impl AsRef, new: impl AsRef) -> b } else { false } - #[cfg(windows)] - { - // TODO: use MoveFileEx without MOVEFILE_REPLACE_EXISTING - } +} + +#[inline] +#[cfg(windows)] +pub async fn rename_without_overwriting( + old: impl AsRef, + new: impl AsRef, +) -> io::Result<()> { + use widestring::U16CString; + use windows_sys::Win32::Storage::FileSystem::MoveFileExW; + let old = U16CString::from_os_str(old.as_ref().as_os_str()) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid path"))?; + let new = U16CString::from_os_str(new.as_ref().as_os_str()) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid path"))?; + let result = unsafe { MoveFileExW(old.as_ptr(), new.as_ptr(), 0) }; + if result != 0 { Ok(()) } else { Err(io::Error::last_os_error()) } } pub async fn symlink_realpath(path: &Path) -> Result {