From 9ae8b90d2328139622e6e1285889310adb57b81b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Sun, 7 Dec 2025 18:00:49 +0800 Subject: [PATCH] feat: prioritize symbolic links when cutting remote files (#3412) --- yazi-fs/src/provider/capabilities.rs | 4 +++ yazi-fs/src/provider/local/local.rs | 5 +++- yazi-fs/src/provider/mod.rs | 2 +- yazi-fs/src/provider/traits.rs | 4 ++- yazi-scheduler/src/file/file.rs | 41 ++++++++++++++++++++++------ yazi-scheduler/src/file/in.rs | 23 +++++++++++++--- yazi-scheduler/src/file/traverse.rs | 5 ++-- yazi-scheduler/src/scheduler.rs | 2 +- yazi-vfs/src/provider/provider.rs | 9 +++++- yazi-vfs/src/provider/providers.rs | 9 +++++- yazi-vfs/src/provider/sftp/sftp.rs | 4 ++- 11 files changed, 87 insertions(+), 21 deletions(-) create mode 100644 yazi-fs/src/provider/capabilities.rs diff --git a/yazi-fs/src/provider/capabilities.rs b/yazi-fs/src/provider/capabilities.rs new file mode 100644 index 00000000..323bbf95 --- /dev/null +++ b/yazi-fs/src/provider/capabilities.rs @@ -0,0 +1,4 @@ +#[derive(Clone, Copy, Debug)] +pub struct Capabilities { + pub symlink: bool, +} diff --git a/yazi-fs/src/provider/local/local.rs b/yazi-fs/src/provider/local/local.rs index 53ce13a1..9c9d1951 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, path::absolute_url, provider::{Attrs, Provider}}; +use crate::{cha::Cha, path::absolute_url, provider::{Attrs, Capabilities, Provider}}; #[derive(Clone)] pub struct Local<'a> { @@ -25,6 +25,9 @@ impl<'a> Provider for Local<'a> { tokio::fs::canonicalize(self.path).await.map(Into::into) } + #[inline] + fn capabilities(&self) -> Capabilities { Capabilities { symlink: true } } + async fn casefold(&self) -> io::Result { super::casefold(self.path).await.map(Into::into) } diff --git a/yazi-fs/src/provider/mod.rs b/yazi-fs/src/provider/mod.rs index 3297b1c6..1a96a542 100644 --- a/yazi-fs/src/provider/mod.rs +++ b/yazi-fs/src/provider/mod.rs @@ -1,3 +1,3 @@ yazi_macro::mod_pub!(local); -yazi_macro::mod_flat!(attrs traits); +yazi_macro::mod_flat!(attrs capabilities traits); diff --git a/yazi-fs/src/provider/traits.rs b/yazi-fs/src/provider/traits.rs index cfe2db55..9cb591c4 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}; +use crate::{cha::{Cha, ChaType}, provider::{Attrs, Capabilities}}; pub trait Provider: Sized { type File: AsyncRead + AsyncSeek + AsyncWrite + Unpin; @@ -17,6 +17,8 @@ pub trait Provider: Sized { fn canonicalize(&self) -> impl Future>; + fn capabilities(&self) -> Capabilities; + fn casefold(&self) -> impl Future>; fn copy

(&self, to: P, attrs: Attrs) -> impl Future> diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 9b7f8df8..8144eea5 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -53,7 +53,7 @@ impl File { } pub(crate) async fn copy_do(&self, mut task: FileInCopy) -> Result<(), FileOutCopyDo> { - ok_or_not_found!(provider::remove_file(&task.to).await); // FIXME: integrate into copy_with_progress + ok_or_not_found!(provider::remove_file(&task.to).await); let mut it = provider::copy_with_progress(&task.from, &task.to, task.cha.unwrap()).await?; while let Some(res) = it.recv().await { @@ -87,20 +87,30 @@ impl File { return Ok(self.ops.out(id, FileOutCut::Succ)); } + let (mut links, mut files) = (vec![], vec![]); + let reorder = task.follow && provider::capabilities(&task.from).await?.symlink; + super::traverse::( task, async |dir| match provider::create_dir(dir).await { Err(e) if e.kind() != AlreadyExists => Err(e)?, _ => Ok(()), }, - async |task, cha| { - Ok(if cha.is_orphan() || (cha.is_link() && !task.follow) { - self.ops.out(id, FileOutCut::New(0)); + |task, cha| { + let nofollow = cha.is_orphan() || (cha.is_link() && !task.follow); + self.ops.out(id, FileOutCut::New(if nofollow { 0 } else { cha.len })); + + if nofollow { self.queue(task.into_link(), NORMAL); } else { - self.ops.out(id, FileOutCut::New(cha.len)); - self.queue(task, LOW); - }) + match (cha.is_link(), reorder) { + (_, false) => self.queue(task, LOW), + (true, true) => links.push(task), + (false, true) => files.push(task), + } + }; + + async { Ok(()) } }, |err| { self.ops.out(id, FileOutCut::Deform(err)); @@ -108,11 +118,26 @@ impl File { ) .await?; + if !links.is_empty() { + let len = links.len(); + let (tx, mut rx) = mpsc::channel(len); + for task in links { + self.queue(task.with_drop(&tx), LOW); + } + for _ in 0..len { + rx.recv().await; + } + } + + for task in files { + self.queue(task, LOW); + } + Ok(self.ops.out(id, FileOutCut::Succ)) } pub(crate) async fn cut_do(&self, mut task: FileInCut) -> Result<(), FileOutCutDo> { - ok_or_not_found!(provider::remove_file(&task.to).await); // FIXME: integrate into copy_with_progress + ok_or_not_found!(provider::remove_file(&task.to).await); let mut it = provider::copy_with_progress(&task.from, &task.to, task.cha.unwrap()).await?; while let Some(res) = it.recv().await { diff --git a/yazi-scheduler/src/file/in.rs b/yazi-scheduler/src/file/in.rs index 0a447094..91483054 100644 --- a/yazi-scheduler/src/file/in.rs +++ b/yazi-scheduler/src/file/in.rs @@ -1,5 +1,6 @@ -use std::path::PathBuf; +use std::{mem, path::PathBuf}; +use tokio::sync::mpsc; use yazi_fs::cha::Cha; use yazi_shared::{Id, url::UrlBuf}; @@ -37,20 +38,34 @@ pub(crate) struct FileInCut { pub(crate) cha: Option, pub(crate) follow: bool, pub(crate) retry: u8, + pub(crate) drop: Option>, +} + +impl Drop for FileInCut { + fn drop(&mut self) { + if let Some(tx) = self.drop.take() { + tx.try_send(()).ok(); + } + } } impl FileInCut { - pub(super) fn into_link(self) -> FileInLink { + pub(super) fn into_link(mut self) -> FileInLink { FileInLink { id: self.id, - from: self.from, - to: self.to, + from: mem::take(&mut self.from), + to: mem::take(&mut self.to), cha: self.cha, resolve: true, relative: false, delete: true, } } + + pub(super) fn with_drop(mut self, drop: &mpsc::Sender<()>) -> Self { + self.drop = Some(drop.clone()); + self + } } // --- Link diff --git a/yazi-scheduler/src/file/traverse.rs b/yazi-scheduler/src/file/traverse.rs index 0336da0e..bd30db80 100644 --- a/yazi-scheduler/src/file/traverse.rs +++ b/yazi-scheduler/src/file/traverse.rs @@ -61,6 +61,7 @@ impl Traverse for FileInCut { cha: Some(cha), follow: self.follow, retry: self.retry, + drop: self.drop.clone(), } } @@ -137,14 +138,14 @@ impl Traverse for FileInUpload { pub(super) async fn traverse( mut task: T, on_dir: D, - on_file: FC, + mut on_file: FC, on_error: E, ) -> Result<(), R> where R: Debug + From, T: Traverse, D: AsyncFn(Url) -> Result<(), R>, - FC: Fn(T, Cha) -> FR, + FC: FnMut(T, Cha) -> FR, FR: Future>, E: Fn(String), { diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index 464b90a0..1d489bc4 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -102,7 +102,7 @@ impl Scheduler { if !force { to = unique_name(to, must_be_dir(&from)).await?; } - file.cut(FileInCut { id, from, to, cha: None, follow, retry: 0 }).await + file.cut(FileInCut { id, from, to, cha: None, follow, retry: 0, drop: None }).await }); } diff --git a/yazi-vfs/src/provider/provider.rs b/yazi-vfs/src/provider/provider.rs index 218f9ca3..ca2d6d9e 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, Provider, local::Local}}; +use yazi_fs::{cha::Cha, provider::{Attrs, Capabilities, Provider, local::Local}}; use yazi_shared::{path::PathBufDyn, strand::AsStrand, url::{AsUrl, UrlBuf, UrlCow}}; use super::{Providers, ReadDir, RwFile}; @@ -32,6 +32,13 @@ where Providers::new(url.as_url()).await?.canonicalize().await } +pub async fn capabilities(url: U) -> io::Result +where + U: AsUrl, +{ + Ok(Providers::new(url.as_url()).await?.capabilities()) +} + pub async fn casefold(url: U) -> io::Result where U: AsUrl, diff --git a/yazi-vfs/src/provider/providers.rs b/yazi-vfs/src/provider/providers.rs index e66b8356..7a6f8243 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, Provider}}; +use yazi_fs::{cha::Cha, provider::{Attrs, Capabilities, Provider}}; use yazi_shared::{path::{AsPath, PathBufDyn}, strand::AsStrand, url::{Url, UrlBuf, UrlCow}}; #[derive(Clone)] @@ -31,6 +31,13 @@ impl<'a> Provider for Providers<'a> { } } + fn capabilities(&self) -> Capabilities { + match self { + Self::Local(p) => p.capabilities(), + Self::Sftp(p) => p.capabilities(), + } + } + async fn casefold(&self) -> io::Result { match self { Self::Local(p) => p.casefold().await, diff --git a/yazi-vfs/src/provider/sftp/sftp.rs b/yazi-vfs/src/provider/sftp/sftp.rs index db44f0a5..7078076d 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::{CWD, provider::{DirReader, FileHolder, Provider}}; +use yazi_fs::{CWD, 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}}; @@ -43,6 +43,8 @@ impl<'a> Provider for Sftp<'a> { }) } + fn capabilities(&self) -> Capabilities { Capabilities { symlink: true } } + async fn casefold(&self) -> io::Result { let Some((parent, name)) = self.url.parent().zip(self.url.name()) else { return Ok(self.url.to_owned());