From 205b7fe11fdc5780e65779bb1e0350f3efc1c0a8 Mon Sep 17 00:00:00 2001 From: sxyazi Date: Fri, 5 Dec 2025 15:04:39 +0800 Subject: [PATCH] refactor: rename the "provider" term in VFS config to "service" (#3403) --- Cargo.lock | 8 +- yazi-config/src/vfs/mod.rs | 2 +- .../src/vfs/{provider.rs => service.rs} | 16 ++-- yazi-config/src/vfs/vfs.rs | 18 ++--- yazi-fs/src/path/path.rs | 56 -------------- yazi-fs/src/path/relative.rs | 77 ++++++++++++++----- yazi-plugin/preset/plugins/fzf.lua | 5 +- yazi-plugin/preset/plugins/mime-remote.lua | 4 +- yazi-scheduler/src/file/file.rs | 26 +++---- yazi-shared/src/path/cow.rs | 4 + yazi-vfs/src/provider/provider.rs | 19 ++--- yazi-vfs/src/provider/sftp/conn.rs | 4 +- yazi-vfs/src/provider/sftp/gate.rs | 4 +- yazi-vfs/src/provider/sftp/mod.rs | 2 +- yazi-vfs/src/provider/sftp/sftp.rs | 6 +- 15 files changed, 119 insertions(+), 132 deletions(-) rename yazi-config/src/vfs/{provider.rs => service.rs} (83%) diff --git a/Cargo.lock b/Cargo.lock index 4303d578..58543723 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1305,9 +1305,9 @@ checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "30f4148e3c9b7dbe0cc7e842ad5a61b28f9025f201d78149383e778a08bc9215" dependencies = [ "crc32fast", "miniz_oxide", @@ -2136,9 +2136,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", diff --git a/yazi-config/src/vfs/mod.rs b/yazi-config/src/vfs/mod.rs index b27c77fc..e52b3881 100644 --- a/yazi-config/src/vfs/mod.rs +++ b/yazi-config/src/vfs/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(provider vfs); +yazi_macro::mod_flat!(service vfs); diff --git a/yazi-config/src/vfs/provider.rs b/yazi-config/src/vfs/service.rs similarity index 83% rename from yazi-config/src/vfs/provider.rs rename to yazi-config/src/vfs/service.rs index 544e73ac..1a6f5126 100644 --- a/yazi-config/src/vfs/provider.rs +++ b/yazi-config/src/vfs/service.rs @@ -5,21 +5,21 @@ use yazi_fs::path::expand_url; #[derive(Deserialize, Serialize)] #[serde(tag = "type", rename_all = "kebab-case")] -pub enum Provider { - Sftp(ProviderSftp), +pub enum Service { + Sftp(ServiceSftp), } -impl TryFrom<&'static Provider> for &'static ProviderSftp { +impl TryFrom<&'static Service> for &'static ServiceSftp { type Error = &'static str; - fn try_from(value: &'static Provider) -> Result { + fn try_from(value: &'static Service) -> Result { match value { - Provider::Sftp(p) => Ok(p), + Service::Sftp(p) => Ok(p), } } } -impl Provider { +impl Service { pub(super) fn reshape(&mut self) -> io::Result<()> { match self { Self::Sftp(p) => p.reshape(), @@ -29,7 +29,7 @@ impl Provider { // --- SFTP #[derive(Deserialize, Eq, Hash, PartialEq, Serialize)] -pub struct ProviderSftp { +pub struct ServiceSftp { pub host: String, pub user: String, pub port: u16, @@ -41,7 +41,7 @@ pub struct ProviderSftp { pub identity_agent: PathBuf, } -impl ProviderSftp { +impl ServiceSftp { fn reshape(&mut self) -> io::Result<()> { if !self.key_file.as_os_str().is_empty() { self.key_file = expand_url(&self.key_file) diff --git a/yazi-config/src/vfs/vfs.rs b/yazi-config/src/vfs/vfs.rs index 3459ad82..c5039ac8 100644 --- a/yazi-config/src/vfs/vfs.rs +++ b/yazi-config/src/vfs/vfs.rs @@ -6,11 +6,11 @@ use tokio::sync::OnceCell; use yazi_fs::Xdg; use yazi_macro::ok_or_not_found; -use super::Provider; +use super::Service; #[derive(Deserialize, Serialize)] pub struct Vfs { - pub providers: HashMap, + pub services: HashMap, } impl Vfs { @@ -27,16 +27,16 @@ impl Vfs { LOADED.get_or_try_init(init).await } - pub async fn provider<'a, P>(name: &str) -> io::Result<(&'a str, P)> + pub async fn service<'a, P>(name: &str) -> io::Result<(&'a str, P)> where - P: TryFrom<&'a Provider, Error = &'static str>, + P: TryFrom<&'a Service, Error = &'static str>, { - let Some((key, value)) = Self::load().await?.providers.get_key_value(name) else { - return Err(io::Error::other(format!("No such VFS provider: {name}"))); + let Some((key, value)) = Self::load().await?.services.get_key_value(name) else { + return Err(io::Error::other(format!("No such VFS service: {name}"))); }; match value.try_into() { Ok(p) => Ok((key.as_str(), p)), - Err(e) => Err(io::Error::other(format!("VFS provider `{key}` has wrong type: {e}"))), + Err(e) => Err(io::Error::other(format!("VFS service `{key}` has wrong type: {e}"))), } } @@ -48,14 +48,14 @@ impl Vfs { } fn reshape(mut self) -> io::Result { - for (name, provider) in &mut self.providers { + for (name, service) in &mut self.services { if name.is_empty() || name.len() > 20 { Err(io::Error::other(format!("VFS name `{name}` must be between 1 and 20 characters")))?; } else if !name.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-')) { Err(io::Error::other(format!("VFS name `{name}` must be in kebab-case")))?; } - provider.reshape()?; + service.reshape()?; } Ok(self) diff --git a/yazi-fs/src/path/path.rs b/yazi-fs/src/path/path.rs index f59eb489..c1b46fcd 100644 --- a/yazi-fs/src/path/path.rs +++ b/yazi-fs/src/path/path.rs @@ -9,59 +9,3 @@ pub fn skip_url(url: &UrlBuf, n: usize) -> StrandCow<'_> { } it.strand() } - -#[cfg(test)] -mod tests { - use yazi_shared::url::{AsUrl, UrlCow}; - - use crate::path::url_relative_to; - - #[test] - fn test_url_relative_to() { - yazi_shared::init_tests(); - - #[cfg(unix)] - let cases = [ - // Same urls - ("", "", "."), - (".", ".", "."), - ("/a", "/a", "."), - ("regular:///", "/", "."), - ("regular://", "regular://", "."), - ("regular://", "search://kw/", "search://kw/."), - ("regular:///b", "search://kw//b", "search://kw/."), - // Relative urls - ("foo", "bar", "../bar"), - // Absolute urls - ("/a/b/c", "/a/b", ".."), - ("/a/b", "/a/b/c", "c"), - ("/a/b/d", "/a/b/c", "../c"), - ("/a/b/c", "/a", "../.."), - ("/a/b/b", "/a/a/b", "../../a/b"), - ("regular:///a/b", "regular:///a/b/c", "c"), - ("/a/b/c/", "search://kw//a/d/", "search://kw/../../d"), - ("search://kw//a/b/c", "search://kw//a/b", "search://kw/.."), - // Different schemes - ("", "sftp://test/", "sftp://test/"), - ("a", "sftp://test/", "sftp://test/"), - ("a", "sftp://test/b", "sftp://test/b"), - ("/a", "sftp://test//b", "sftp://test//b"), - ("sftp://test//a/b", "sftp://test//a/d", "sftp://test:0:0/../d"), - ]; - - #[cfg(windows)] - let cases = [ - (r"C:\a\b\c", r"C:\a\b", r".."), - (r"C:\a\b", r"C:\a\b\c", "c"), - (r"C:\a\b\d", r"C:\a\b\c", r"..\c"), - (r"C:\a\b\c", r"C:\a", r"..\.."), - (r"C:\a\b\b", r"C:\a\a\b", r"..\..\a\b"), - ]; - - for (from, to, expected) in cases { - let from: UrlCow = from.try_into().unwrap(); - let to: UrlCow = to.try_into().unwrap(); - assert_eq!(format!("{:?}", url_relative_to(from, to).unwrap().as_url()), expected); - } - } -} diff --git a/yazi-fs/src/path/relative.rs b/yazi-fs/src/path/relative.rs index 172ec187..30a2b8e4 100644 --- a/yazi-fs/src/path/relative.rs +++ b/yazi-fs/src/path/relative.rs @@ -1,39 +1,35 @@ use anyhow::{Result, bail}; -use yazi_shared::{path::PathBufDyn, url::{UrlCow, UrlLike}}; +use yazi_shared::path::{PathBufDyn, PathCow, PathDyn, PathLike}; -pub fn url_relative_to<'a, 'b, U, V>(from: U, to: V) -> Result> +pub fn path_relative_to<'a, 'b, P, Q>(from: P, to: Q) -> Result> where - U: Into>, - V: Into>, + P: Into>, + Q: Into>, { - url_relative_to_(from.into(), to.into()) + path_relative_to_impl(from.into(), to.into()) } -fn url_relative_to_<'a>(from: UrlCow<'_>, to: UrlCow<'a>) -> Result> { - use yazi_shared::url::Component::*; +fn path_relative_to_impl<'a>(from: PathCow<'_>, to: PathCow<'a>) -> Result> { + use yazi_shared::path::Component::*; if from.is_absolute() != to.is_absolute() { return if to.is_absolute() { Ok(to) } else { - bail!("Urls must be both absolute or both relative: {from:?} and {to:?}"); + bail!("Paths must be both absolute or both relative: {from:?} and {to:?}"); }; } - if from.covariant(&to) { - return UrlCow::try_from(( - to.scheme().zeroed().to_owned(), - PathBufDyn::with_str(to.kind(), "."), - )); + if from == to { + return Ok(PathDyn::with_str(to.kind(), ".").into()); } let (mut f_it, mut t_it) = (from.components(), to.components()); let (f_head, t_head) = loop { match (f_it.next(), t_it.next()) { - (Some(Scheme(a)), Some(Scheme(b))) if a.covariant(b) => {} (Some(RootDir), Some(RootDir)) => {} (Some(Prefix(a)), Some(Prefix(b))) if a == b => {} - (Some(Scheme(_) | Prefix(_) | RootDir), _) | (_, Some(Scheme(_) | Prefix(_) | RootDir)) => { + (Some(Prefix(_) | RootDir), _) | (_, Some(Prefix(_) | RootDir)) => { return Ok(to); } (None, None) => break (None, None), @@ -45,8 +41,51 @@ fn url_relative_to_<'a>(from: UrlCow<'_>, to: UrlCow<'a>) -> Result> let dots = f_head.into_iter().chain(f_it).map(|_| ParentDir); let rest = t_head.into_iter().chain(t_it); - let iter = dots.chain(rest).map(|c| c.downgrade().expect("path component from dot or normal")); - let buf = PathBufDyn::from_components(to.kind(), iter)?; - - UrlCow::try_from((to.scheme().zeroed().to_owned(), buf)) + let buf = PathBufDyn::from_components(to.kind(), dots.chain(rest))?; + Ok(buf.into()) +} + +#[cfg(test)] +mod tests { + use yazi_shared::path::PathDyn; + + use super::*; + + #[test] + fn test_path_relative_to() { + yazi_shared::init_tests(); + + #[cfg(unix)] + let cases = [ + // Same paths + ("", "", "."), + (".", ".", "."), + ("/", "/", "."), + ("/a", "/a", "."), + // Relative paths + ("foo", "bar", "../bar"), + // Absolute paths + ("/a/b", "/a/b/c", "c"), + ("/a/b/c", "/a/b", ".."), + ("/a/b/d", "/a/b/c", "../c"), + ("/a/b/c", "/a", "../.."), + ("/a/b/c/", "/a/d/", "../../d"), + ("/a/b/b", "/a/a/b", "../../a/b"), + ]; + + #[cfg(windows)] + let cases = [ + (r"C:\a\b", r"C:\a\b\c", "c"), + (r"C:\a\b\c", r"C:\a\b", r".."), + (r"C:\a\b\d", r"C:\a\b\c", r"..\c"), + (r"C:\a\b\c", r"C:\a", r"..\.."), + (r"C:\a\b\b", r"C:\a\a\b", r"..\..\a\b"), + ]; + + for (from, to, expected) in cases { + let from = PathDyn::Os(from.as_ref()); + let to = PathDyn::Os(to.as_ref()); + assert_eq!(path_relative_to(from, to).unwrap().to_str().unwrap(), expected); + } + } } diff --git a/yazi-plugin/preset/plugins/fzf.lua b/yazi-plugin/preset/plugins/fzf.lua index 3de191bb..ca239c43 100644 --- a/yazi-plugin/preset/plugins/fzf.lua +++ b/yazi-plugin/preset/plugins/fzf.lua @@ -11,9 +11,12 @@ end) function M:entry() ya.emit("escape", { visual = true }) - local _permit = ui.hide() local cwd, selected = state() + if cwd.scheme.is_virtual then + return ya.notify { title = "Fzf", content = "Not supported under virtual filesystems", timeout = 5, level = "warn" } + end + local _permit = ui.hide() local output, err = M.run_with(cwd, selected) if not output then return ya.notify { title = "Fzf", content = tostring(err), timeout = 5, level = "error" } diff --git a/yazi-plugin/preset/plugins/mime-remote.lua b/yazi-plugin/preset/plugins/mime-remote.lua index 581e675d..d936fe5c 100644 --- a/yazi-plugin/preset/plugins/mime-remote.lua +++ b/yazi-plugin/preset/plugins/mime-remote.lua @@ -17,7 +17,9 @@ end function M:fetch(job) local updates, unknown, state = {}, {}, {} for i, file in ipairs(job.files) do - if not file.cache then + if file.cha.is_dummy then + -- Skip dummy files + elseif not file.cache then unknown[#unknown + 1] = file elseif not fs.cha(file.cache) then updates[file.url], state[i] = "vfs/absent", true diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 075749cb..da141ab2 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -1,10 +1,10 @@ -use std::{collections::VecDeque, hash::{BuildHasher, Hash, Hasher}}; +use std::{collections::VecDeque, hash::{BuildHasher, Hash, Hasher}, time::Duration}; use anyhow::{Context, Result, anyhow}; use tokio::{io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; use tracing::warn; use yazi_config::YAZI; -use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, ok_or_not_found, path::{skip_url, url_relative_to}, provider::{Attrs, DirReader, FileHolder, Provider, local::Local}}; +use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, ok_or_not_found, path::{path_relative_to, skip_url}, provider::{Attrs, DirReader, FileHolder, Provider, local::Local}}; use yazi_macro::ok_or_not_found; use yazi_shared::{path::PathCow, timestamp_us, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; use yazi_vfs::{VfsCha, maybe_exists, provider::{self, DirEntry}, unique_name}; @@ -132,27 +132,27 @@ impl File { } pub(crate) async fn link_do(&self, task: FileInLink) -> Result<(), FileOutLink> { - let src: PathCow = if task.resolve { + if !task.from.scheme().covariant(task.to.scheme()) { + Err(anyhow!("Source and target must be on the same filesystem: {task:?}"))? + } + + let mut src: PathCow = if task.resolve { ok_or_not_found!( provider::read_link(&task.from).await, return Ok(self.ops.out(task.id, FileOutLink::Succ)) ) .into() - } else if task.from.scheme().covariant(task.to.scheme()) { - task.from.loc().into() } else { - Err(anyhow!("Source and target must be on the same filesystem: {task:?}"))? + task.from.loc().into() }; - let src = UrlCow::try_from((task.from.scheme(), src))?; - let src = if task.relative { - url_relative_to(provider::canonicalize(task.to.parent().unwrap()).await?, &src)? - } else { - src - }; + if task.relative { + let canon = provider::canonicalize(task.to.parent().unwrap()).await?; + src = path_relative_to(canon.loc(), src)?; + } ok_or_not_found!(provider::remove_file(&task.to).await); - provider::symlink(&src, &task.to, async || { + provider::symlink(task.to, src, async || { Ok(match task.cha { Some(cha) => cha.is_dir(), None => Self::cha(&task.from, task.resolve, None).await?.is_dir(), diff --git a/yazi-shared/src/path/cow.rs b/yazi-shared/src/path/cow.rs index fe3f1073..dc8a0a32 100644 --- a/yazi-shared/src/path/cow.rs +++ b/yazi-shared/src/path/cow.rs @@ -27,6 +27,10 @@ impl From> for PathBufDyn { fn from(value: PathCow<'_>) -> Self { value.into_owned() } } +impl PartialEq for PathCow<'_> { + fn eq(&self, other: &Self) -> bool { self.as_path() == other.as_path() } +} + impl PartialEq<&str> for PathCow<'_> { fn eq(&self, other: &&str) -> bool { match self { diff --git a/yazi-vfs/src/provider/provider.rs b/yazi-vfs/src/provider/provider.rs index d151dea0..4a93a644 100644 --- a/yazi-vfs/src/provider/provider.rs +++ b/yazi-vfs/src/provider/provider.rs @@ -201,32 +201,27 @@ where } } -pub async fn symlink(original: U, link: V, is_dir: F) -> io::Result<()> +pub async fn symlink(link: U, original: P, is_dir: F) -> io::Result<()> where U: AsUrl, - V: AsUrl, + P: AsPath, F: AsyncFnOnce() -> io::Result, { - let (original, link) = (original.as_url(), link.as_url()); - if original.scheme().covariant(link.scheme()) { - Providers::new(link).await?.symlink(original.loc(), is_dir).await - } else { - Err(io::Error::from(io::ErrorKind::CrossesDevices)) - } + Providers::new(link.as_url()).await?.symlink(original, is_dir).await } -pub async fn symlink_dir(original: P, link: U) -> io::Result<()> +pub async fn symlink_dir(link: U, original: P) -> io::Result<()> where - P: AsPath, U: AsUrl, + P: AsPath, { Providers::new(link.as_url()).await?.symlink_dir(original).await } -pub async fn symlink_file(original: P, link: U) -> io::Result<()> +pub async fn symlink_file(link: U, original: P) -> io::Result<()> where - P: AsPath, U: AsUrl, + P: AsPath, { Providers::new(link.as_url()).await?.symlink_file(original).await } diff --git a/yazi-vfs/src/provider/sftp/conn.rs b/yazi-vfs/src/provider/sftp/conn.rs index e3d2672e..251b41ec 100644 --- a/yazi-vfs/src/provider/sftp/conn.rs +++ b/yazi-vfs/src/provider/sftp/conn.rs @@ -1,13 +1,13 @@ use std::{io, sync::Arc}; use russh::keys::PrivateKeyWithHashAlg; -use yazi_config::vfs::ProviderSftp; +use yazi_config::vfs::ServiceSftp; use yazi_fs::provider::local::Local; #[derive(Clone, Copy)] pub(super) struct Conn { pub(super) name: &'static str, - pub(super) config: &'static ProviderSftp, + pub(super) config: &'static ServiceSftp, } impl russh::client::Handler for Conn { diff --git a/yazi-vfs/src/provider/sftp/gate.rs b/yazi-vfs/src/provider/sftp/gate.rs index 4585b531..16c4048a 100644 --- a/yazi-vfs/src/provider/sftp/gate.rs +++ b/yazi-vfs/src/provider/sftp/gate.rs @@ -1,6 +1,6 @@ use std::io; -use yazi_config::vfs::{ProviderSftp, Vfs}; +use yazi_config::vfs::{ServiceSftp, Vfs}; use yazi_fs::provider::{Attrs, FileBuilder}; use yazi_sftp::fs::Flags; use yazi_shared::url::{AsUrl, Url}; @@ -64,7 +64,7 @@ impl FileBuilder for Gate { { let url = url.as_url(); let (path, (name, config)) = match url { - Url::Sftp { loc, domain } => (*loc, Vfs::provider::<&ProviderSftp>(domain).await?), + Url::Sftp { loc, domain } => (*loc, Vfs::service::<&ServiceSftp>(domain).await?), _ => Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not an SFTP URL: {url:?}")))?, }; diff --git a/yazi-vfs/src/provider/sftp/mod.rs b/yazi-vfs/src/provider/sftp/mod.rs index 2d9809ae..cef2f5ab 100644 --- a/yazi-vfs/src/provider/sftp/mod.rs +++ b/yazi-vfs/src/provider/sftp/mod.rs @@ -3,7 +3,7 @@ yazi_macro::mod_flat!(conn gate metadata read_dir sftp); static CONN: yazi_shared::RoCell< parking_lot::Mutex< hashbrown::HashMap< - &'static yazi_config::vfs::ProviderSftp, + &'static yazi_config::vfs::ServiceSftp, &'static deadpool::managed::Pool, >, >, diff --git a/yazi-vfs/src/provider/sftp/sftp.rs b/yazi-vfs/src/provider/sftp/sftp.rs index 87c9928f..6fdc063a 100644 --- a/yazi-vfs/src/provider/sftp/sftp.rs +++ b/yazi-vfs/src/provider/sftp/sftp.rs @@ -1,7 +1,7 @@ use std::{io, sync::Arc}; use tokio::{io::{AsyncWriteExt, BufReader, BufWriter}, sync::mpsc::Receiver}; -use yazi_config::vfs::{ProviderSftp, Vfs}; +use yazi_config::vfs::{ServiceSftp, Vfs}; use yazi_fs::{CWD, provider::{DirReader, FileHolder, Provider}}; use yazi_sftp::fs::{Attrs, Flags}; use yazi_shared::{loc::LocBuf, path::{AsPath, PathBufDyn}, pool::InternStr, scheme::SchemeKind, url::{Url, UrlBuf, UrlCow, UrlLike}}; @@ -15,7 +15,7 @@ pub struct Sftp<'a> { path: &'a typed_path::UnixPath, name: &'static str, - config: &'static ProviderSftp, + config: &'static ServiceSftp, } impl<'a> Provider for Sftp<'a> { @@ -136,7 +136,7 @@ impl<'a> Provider for Sftp<'a> { Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a SFTP URL: {url:?}"))) } Url::Sftp { loc, domain } => { - let (name, config) = Vfs::provider::<&ProviderSftp>(domain).await?; + let (name, config) = Vfs::service::<&ServiceSftp>(domain).await?; Ok(Self::Me { url, path: loc.as_inner(), name, config }) } }