From ae4c138f49e00a64b478318ed9c7e9072fef8c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Fri, 17 Apr 2026 00:16:29 +0800 Subject: [PATCH] fix: `paste --force` doesn't work on existing read-only files with the same name (#3894) --- CHANGELOG.md | 2 ++ yazi-actor/src/mgr/shell.rs | 6 +++--- yazi-fs/Cargo.toml | 2 +- yazi-fs/src/cha/cha.rs | 22 ++++++++++++---------- yazi-fs/src/provider/local/local.rs | 22 +++++++++++++++++++++- yazi-fs/src/provider/traits.rs | 4 +++- yazi-scheduler/src/file/transaction.rs | 7 ++++++- yazi-vfs/src/provider/provider.rs | 9 ++++++++- yazi-vfs/src/provider/providers.rs | 9 ++++++++- yazi-vfs/src/provider/sftp/sftp.rs | 10 +++++++++- 10 files changed, 73 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40ff9a3b..a123698a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): ### Fixed - Chafa v1.18.1 causes random ghost keypresses when previewing images ([#3678]) +- `paste --force` doesn't work on existing read-only files with the same name ([#3894]) - Be a little defensive while parsing the output of `7zz -ba` ([#3744]) - Make `ya pkg` ignore default remote name in user Git config ([#3648]) - Archive extraction fails for target paths with non-ASCII characters on Windows ([#3607]) @@ -1701,3 +1702,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#3854]: https://github.com/sxyazi/yazi/pull/3854 [#3862]: https://github.com/sxyazi/yazi/pull/3862 [#3891]: https://github.com/sxyazi/yazi/pull/3891 +[#3894]: https://github.com/sxyazi/yazi/pull/3894 diff --git a/yazi-actor/src/mgr/shell.rs b/yazi-actor/src/mgr/shell.rs index de091fb9..d8877bac 100644 --- a/yazi-actor/src/mgr/shell.rs +++ b/yazi-actor/src/mgr/shell.rs @@ -41,9 +41,9 @@ impl Actor for Shell { TasksProxy::open_shell_compat(ProcessOpt { cwd, - cmd: form.run.to_string().into(), - args: selected, - block: form.block, + cmd: form.run.to_string().into(), + args: selected, + block: form.block, orphan: form.orphan, spread: true, }); diff --git a/yazi-fs/Cargo.toml b/yazi-fs/Cargo.toml index d0ca5567..68e741c6 100644 --- a/yazi-fs/Cargo.toml +++ b/yazi-fs/Cargo.toml @@ -26,6 +26,7 @@ dirs = { workspace = true } either = { workspace = true } foldhash = { workspace = true } hashbrown = { workspace = true } +libc = { workspace = true } parking_lot = { workspace = true } percent-encoding = { workspace = true } rand = { workspace = true } @@ -38,7 +39,6 @@ tracing = { workspace = true } typed-path = { workspace = true } [target."cfg(unix)".dependencies] -libc = { workspace = true } uzers = { workspace = true } [target.'cfg(windows)'.dependencies] diff --git a/yazi-fs/src/cha/cha.rs b/yazi-fs/src/cha/cha.rs index f8ebb49d..8e4f1dfe 100644 --- a/yazi-fs/src/cha/cha.rs +++ b/yazi-fs/src/cha/cha.rs @@ -81,17 +81,19 @@ impl Cha { }; #[cfg(windows)] - let mode = { - if m.is_file() { - ChaMode::T_FILE - } else if m.is_dir() { - ChaMode::T_DIR - } else if m.is_symlink() { - ChaMode::T_LINK - } else { - ChaMode::empty() - } + let mut mode = if m.is_file() { + ChaMode::T_FILE + } else if m.is_dir() { + ChaMode::T_DIR + } else if m.is_symlink() { + ChaMode::T_LINK + } else { + ChaMode::empty() }; + #[cfg(windows)] + if !m.permissions().readonly() { + mode |= ChaMode::U_WRITE; + } Self { kind: ChaKind::empty(), diff --git a/yazi-fs/src/provider/local/local.rs b/yazi-fs/src/provider/local/local.rs index a9abe8fc..6ec0557d 100644 --- a/yazi-fs/src/provider/local/local.rs +++ b/yazi-fs/src/provider/local/local.rs @@ -3,7 +3,7 @@ use std::{io, path::Path, sync::Arc}; use tokio::sync::mpsc; use yazi_shared::{path::{AsPath, PathBufDyn}, scheme::SchemeKind, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; -use crate::{cha::Cha, provider::{Attrs, Capabilities, Provider}}; +use crate::{cha::{Cha, ChaMode}, provider::{Attrs, Capabilities, Provider}}; #[derive(Clone)] pub struct Local<'a> { @@ -125,6 +125,26 @@ impl<'a> Provider for Local<'a> { tokio::fs::rename(self.path, to).await } + async fn set_mode(&self, mode: ChaMode) -> io::Result<()> { + #[cfg(unix)] + { + return tokio::fs::set_permissions(self.path, mode.into()).await; + } + #[cfg(windows)] + { + use std::os::windows::ffi::OsStrExt; + + let path: Vec = self.path.as_os_str().encode_wide().chain(Some(0)).collect(); + let perm = if mode.contains(ChaMode::U_WRITE) { libc::S_IWRITE } else { libc::S_IREAD }; + + return tokio::task::spawn_blocking(move || { + let result = unsafe { libc::wchmod(path.as_ptr(), perm) }; + if result == 0 { Ok(()) } else { Err(io::Error::last_os_error()) } + }) + .await?; + } + } + #[inline] async fn symlink(&self, original: S, _is_dir: F) -> io::Result<()> where diff --git a/yazi-fs/src/provider/traits.rs b/yazi-fs/src/provider/traits.rs index 28d38f3d..0c9bb0d6 100644 --- a/yazi-fs/src/provider/traits.rs +++ b/yazi-fs/src/provider/traits.rs @@ -4,7 +4,7 @@ use tokio::{io::{AsyncRead, AsyncSeek, AsyncWrite, AsyncWriteExt}, sync::mpsc}; use yazi_macro::ok_or_not_found; use yazi_shared::{path::{AsPath, PathBufDyn}, strand::{AsStrand, StrandCow}, url::{AsUrl, Url, UrlBuf}}; -use crate::{cha::{Cha, ChaType}, provider::{Attrs, Capabilities}}; +use crate::{cha::{Cha, ChaMode, ChaType}, provider::{Attrs, Capabilities}}; pub trait Provider: Sized { type File: AsyncRead + AsyncSeek + AsyncWrite + Unpin; @@ -163,6 +163,8 @@ pub trait Provider: Sized { where P: AsPath; + fn set_mode(&self, mode: ChaMode) -> impl Future>; + fn symlink(&self, original: S, _is_dir: F) -> impl Future> where S: AsStrand, diff --git a/yazi-scheduler/src/file/transaction.rs b/yazi-scheduler/src/file/transaction.rs index 45abfa80..f812af7b 100644 --- a/yazi-scheduler/src/file/transaction.rs +++ b/yazi-scheduler/src/file/transaction.rs @@ -1,5 +1,6 @@ use std::{hash::{BuildHasher, Hash, Hasher}, io}; +use yazi_fs::cha::ChaMode; use yazi_macro::ok_or_not_found; use yazi_shared::{timestamp_us, url::{AsUrl, Url, UrlBuf}}; use yazi_vfs::{provider, unique_file}; @@ -31,8 +32,12 @@ impl Transaction { U: AsUrl, { let url = url.as_url(); - if ok_or_not_found!(provider::symlink_metadata(url).await, return Ok(())).is_link() { + + let cha = ok_or_not_found!(provider::symlink_metadata(url).await, return Ok(())); + if cha.is_link() { provider::rename(Self::tmp(url).await?, url).await?; + } else if !cha.contains(ChaMode::U_WRITE) { + provider::set_mode(url, cha.mode | ChaMode::U_WRITE).await?; } Ok(()) diff --git a/yazi-vfs/src/provider/provider.rs b/yazi-vfs/src/provider/provider.rs index 52d9a011..3535bcb0 100644 --- a/yazi-vfs/src/provider/provider.rs +++ b/yazi-vfs/src/provider/provider.rs @@ -1,7 +1,7 @@ use std::io; use tokio::sync::mpsc; -use yazi_fs::{cha::Cha, provider::{Attrs, Capabilities, Provider, local::Local}}; +use yazi_fs::{cha::{Cha, ChaMode}, provider::{Attrs, Capabilities, Provider, local::Local}}; use yazi_shared::{path::PathBufDyn, strand::AsStrand, url::{AsUrl, Url, UrlBuf, UrlCow}}; use super::{Providers, ReadDir, RwFile}; @@ -215,6 +215,13 @@ where } } +pub async fn set_mode(url: U, mode: ChaMode) -> io::Result<()> +where + U: AsUrl, +{ + Providers::new(url.as_url()).await?.set_mode(mode).await +} + pub async fn symlink(link: U, original: S, is_dir: F) -> io::Result<()> where U: AsUrl, diff --git a/yazi-vfs/src/provider/providers.rs b/yazi-vfs/src/provider/providers.rs index 48aefbbc..5c1d30f5 100644 --- a/yazi-vfs/src/provider/providers.rs +++ b/yazi-vfs/src/provider/providers.rs @@ -1,7 +1,7 @@ use std::io; use tokio::sync::mpsc; -use yazi_fs::{cha::Cha, provider::{Attrs, Capabilities, Provider}}; +use yazi_fs::{cha::{Cha, ChaMode}, provider::{Attrs, Capabilities, Provider}}; use yazi_shared::{path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; #[derive(Clone)] @@ -175,6 +175,13 @@ impl<'a> Provider for Providers<'a> { } } + async fn set_mode(&self, mode: ChaMode) -> io::Result<()> { + match self { + Self::Local(p) => p.set_mode(mode).await, + Self::Sftp(p) => p.set_mode(mode).await, + } + } + async fn symlink(&self, original: S, is_dir: F) -> io::Result<()> where S: AsStrand, diff --git a/yazi-vfs/src/provider/sftp/sftp.rs b/yazi-vfs/src/provider/sftp/sftp.rs index 364e97ed..dc7b8b65 100644 --- a/yazi-vfs/src/provider/sftp/sftp.rs +++ b/yazi-vfs/src/provider/sftp/sftp.rs @@ -2,7 +2,7 @@ use std::{io, sync::Arc}; use tokio::{io::{AsyncWriteExt, BufReader, BufWriter}, sync::mpsc::Receiver}; use yazi_config::vfs::{ServiceSftp, Vfs}; -use yazi_fs::provider::{Capabilities, DirReader, FileHolder, Provider}; +use yazi_fs::{cha::ChaMode, provider::{Capabilities, DirReader, FileHolder, Provider}}; use yazi_sftp::fs::{Attrs, Flags}; use yazi_shared::{loc::LocBuf, path::{AsPath, PathBufDyn}, pool::InternStr, scheme::SchemeKind, strand::AsStrand, url::{Url, UrlBuf, UrlCow, UrlLike}}; @@ -193,6 +193,14 @@ impl<'a> Provider for Sftp<'a> { Ok(()) } + async fn set_mode(&self, mode: ChaMode) -> io::Result<()> { + let attrs = super::Attrs(yazi_fs::provider::Attrs { mode: Some(mode), ..Default::default() }) + .try_into() + .map_err(|()| io::Error::new(io::ErrorKind::InvalidInput, "Cannot convert mode"))?; + + Ok(self.op().await?.setstat(self.path, attrs).await?) + } + async fn symlink(&self, original: S, _is_dir: F) -> io::Result<()> where S: AsStrand,