fix: paste --force doesn't work on existing read-only files with the same name (#3894)

This commit is contained in:
三咲雅 misaki masa 2026-04-17 00:16:29 +08:00 committed by GitHub
parent 463150848d
commit ae4c138f49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 73 additions and 20 deletions

View file

@ -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

View file

@ -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,
});

View file

@ -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]

View file

@ -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(),

View file

@ -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<u16> = 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<S, F>(&self, original: S, _is_dir: F) -> io::Result<()>
where

View file

@ -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<Output = io::Result<()>>;
fn symlink<S, F>(&self, original: S, _is_dir: F) -> impl Future<Output = io::Result<()>>
where
S: AsStrand,

View file

@ -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(())

View file

@ -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<U>(url: U, mode: ChaMode) -> io::Result<()>
where
U: AsUrl,
{
Providers::new(url.as_url()).await?.set_mode(mode).await
}
pub async fn symlink<U, S, F>(link: U, original: S, is_dir: F) -> io::Result<()>
where
U: AsUrl,

View file

@ -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<S, F>(&self, original: S, is_dir: F) -> io::Result<()>
where
S: AsStrand,

View file

@ -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<S, F>(&self, original: S, _is_dir: F) -> io::Result<()>
where
S: AsStrand,