From d39637f40c5261620f7b571c9a2501ca361cc0fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Mon, 13 Jul 2026 20:01:47 +0800 Subject: [PATCH] feat!: support VFS services with same name but different schemes (#4120) --- CHANGELOG.md | 5 ++- yazi-config/preset/vfs-default.toml | 1 - yazi-config/src/vfs/authorities.rs | 48 ++++++++++++++++++++++ yazi-config/src/vfs/domains.rs | 62 +++++++++++++++++++++++++++++ yazi-config/src/vfs/lua.rs | 2 +- yazi-config/src/vfs/mod.rs | 2 +- yazi-config/src/vfs/services.rs | 47 ---------------------- yazi-config/src/vfs/sftp.rs | 6 +-- yazi-config/src/vfs/vfs.rs | 37 ++++++++--------- yazi-shared/src/auth/auth.rs | 2 - yazi-vfs/src/engine/lua/lua.rs | 6 +-- yazi-vfs/src/engine/sftp/sftp.rs | 2 +- 12 files changed, 138 insertions(+), 82 deletions(-) create mode 100644 yazi-config/src/vfs/authorities.rs create mode 100644 yazi-config/src/vfs/domains.rs delete mode 100644 yazi-config/src/vfs/services.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index deded357..09209fa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,9 +31,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): ### Changed +- Rename SFTP sections in `vfs.toml` from `[services.domain]` to `[sftp.domain]` ([#4120]). - Rename `` to `` ([#3989]) -- Rename `type` field in `vfs.toml` to `kind`, leaving `type` available for future custom VFS parameters ([#4118]) -- Remove `Url.is_archive` so `archive://` can be registered as a custom scheme ([#4118]) +- Remove `Url.is_archive` - `archive://` is no longer built in and can now be registered by plugins ([#4118]) - Make `mgr::Yanked`, `tab::Selected`, and the `@yank` DDS event return `File` instead of `Url` from `__pairs()` ([#4096]) - Remove `help:filter` action since the filter input is now always available ([#4074]) - `[help]` of `theme.toml`: supersede `on` with `chord`, supersede `run` and `desc` with `action`, remove `footer` ([#4074]) @@ -1780,3 +1780,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#4104]: https://github.com/sxyazi/yazi/pull/4104 [#4108]: https://github.com/sxyazi/yazi/pull/4108 [#4118]: https://github.com/sxyazi/yazi/pull/4118 +[#4120]: https://github.com/sxyazi/yazi/pull/4120 diff --git a/yazi-config/preset/vfs-default.toml b/yazi-config/preset/vfs-default.toml index 48be1dab..e69de29b 100644 --- a/yazi-config/preset/vfs-default.toml +++ b/yazi-config/preset/vfs-default.toml @@ -1 +0,0 @@ -[services] diff --git a/yazi-config/src/vfs/authorities.rs b/yazi-config/src/vfs/authorities.rs new file mode 100644 index 00000000..23ba0a19 --- /dev/null +++ b/yazi-config/src/vfs/authorities.rs @@ -0,0 +1,48 @@ +use hashbrown::HashMap; +use serde::{Deserialize, Deserializer, de::{MapAccess, Visitor}}; +use yazi_shared::auth::Scheme; +use yazi_shim::toml::DeserializeOverWith; + +use super::{DomainSeed, Domains, Service}; + +pub struct Authorities(HashMap); + +impl Authorities { + pub fn get(&self, scheme: &Scheme, domain: &str) -> Option<&Service> { + self.0.get(scheme)?.get(domain) + } +} + +impl<'de> Deserialize<'de> for Authorities { + fn deserialize>(deserializer: D) -> Result { + struct V; + + impl<'de> Visitor<'de> for V { + type Value = Authorities; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("a map of VFS schemes") + } + + fn visit_map>(self, mut map: M) -> Result { + let mut authorities = HashMap::new(); + while let Some(scheme) = map.next_key()? { + let domains = map.next_value_seed(DomainSeed(&scheme))?; + authorities.insert(scheme, domains); + } + Ok(Authorities(authorities)) + } + } + + deserializer.deserialize_map(V) + } +} + +impl DeserializeOverWith for Authorities { + fn deserialize_over_with<'de, D: Deserializer<'de>>(mut self, de: D) -> Result { + for (scheme, domains) in Self::deserialize(de)?.0 { + self.0.entry(scheme).or_default().extend(domains.0); + } + Ok(self) + } +} diff --git a/yazi-config/src/vfs/domains.rs b/yazi-config/src/vfs/domains.rs new file mode 100644 index 00000000..665e07d3 --- /dev/null +++ b/yazi-config/src/vfs/domains.rs @@ -0,0 +1,62 @@ +use std::{ops::{Deref, DerefMut}, sync::Arc}; + +use hashbrown::HashMap; +use serde::{Deserialize, Deserializer, de::{DeserializeSeed, Error}}; +use yazi_shared::{KebabCasedKey, auth::Scheme}; + +use super::{Service, ServiceSftp}; + +#[derive(Default)] +pub struct Domains(pub(super) HashMap); + +impl Deref for Domains { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl DerefMut for Domains { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +} + +impl Domains { + fn init(&mut self, scheme: &Scheme) { + for (domain, service) in &mut self.0 { + let kind = service.kind(); + let auth = Arc::get_mut(service.auth_mut()).expect("unique auth arc"); + + auth.kind = kind; + auth.scheme = scheme.clone(); + auth.domain = domain.clone().into(); + } + } +} + +// --- DomainSeed +pub(super) struct DomainSeed<'a>(pub &'a Scheme); + +impl<'de> DeserializeSeed<'de> for DomainSeed<'_> { + type Value = Domains; + + fn deserialize>(self, deserializer: D) -> Result { + let mut domains = Domains(match self.0 { + Scheme::Regular | Scheme::Search => { + return Err(D::Error::custom("scheme cannot be configured")); + } + Scheme::Sftp => { + let map = HashMap::::deserialize(deserializer)?; + map.into_iter().map(|(domain, service)| (domain, Service::Sftp(service))).collect() + } + Scheme::Custom(_) => { + let map = HashMap::::deserialize(deserializer)?; + if map.values().any(|service| matches!(service, Service::Sftp(_))) { + return Err(D::Error::custom("SFTP services must use the `sftp` scheme")); + } + map + } + }); + + domains.init(self.0); + Ok(domains) + } +} diff --git a/yazi-config/src/vfs/lua.rs b/yazi-config/src/vfs/lua.rs index f94e2d05..f46f5721 100644 --- a/yazi-config/src/vfs/lua.rs +++ b/yazi-config/src/vfs/lua.rs @@ -5,7 +5,7 @@ use yazi_shared::{auth::Auth, event::Cmd}; #[derive(Deserialize)] pub struct ServiceLua { - #[serde(flatten)] + #[serde(skip, default)] pub auth: Arc, pub run: Cmd, } diff --git a/yazi-config/src/vfs/mod.rs b/yazi-config/src/vfs/mod.rs index 4481f651..4bb8af1a 100644 --- a/yazi-config/src/vfs/mod.rs +++ b/yazi-config/src/vfs/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(lua service services sftp vfs); +yazi_macro::mod_flat!(authorities domains lua service sftp vfs); diff --git a/yazi-config/src/vfs/services.rs b/yazi-config/src/vfs/services.rs deleted file mode 100644 index 46c1d1c8..00000000 --- a/yazi-config/src/vfs/services.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::{ops::{Deref, DerefMut}, sync::Arc}; - -use hashbrown::HashMap; -use serde::{Deserialize, Deserializer}; -use yazi_shared::KebabCasedKey; -use yazi_shim::toml::{DeserializeOverHook, DeserializeOverWith}; - -use super::Service; - -pub struct Services(HashMap); - -impl Deref for Services { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl DerefMut for Services { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } -} - -impl<'de> Deserialize<'de> for Services { - fn deserialize>(deserializer: D) -> Result { - Self(HashMap::deserialize(deserializer)?) - .deserialize_over_hook() - .map_err(serde::de::Error::custom) - } -} - -impl DeserializeOverWith for Services { - fn deserialize_over_with<'de, D: Deserializer<'de>>(self, de: D) -> Result { - Ok(Self(self.0.deserialize_over_with(de)?)) - } -} - -impl DeserializeOverHook for Services { - fn deserialize_over_hook(mut self) -> Result { - for (domain, service) in &mut self.0 { - let kind = service.kind(); - let auth = Arc::get_mut(service.auth_mut()).expect("unique auth arc"); - - auth.kind = kind; - auth.domain = domain.to_string().into(); - } - Ok(self) - } -} diff --git a/yazi-config/src/vfs/sftp.rs b/yazi-config/src/vfs/sftp.rs index 6f3a5615..945c75a1 100644 --- a/yazi-config/src/vfs/sftp.rs +++ b/yazi-config/src/vfs/sftp.rs @@ -2,11 +2,11 @@ use std::{env, ops::Deref, path::PathBuf, sync::Arc}; use serde::{Deserialize, Deserializer, Serialize, de}; use yazi_fs::path::sanitize_path; -use yazi_shared::auth::{Auth, AuthKind, Scheme}; +use yazi_shared::auth::Auth; #[derive(Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct ServiceSftp { - #[serde(skip, default = "default_auth")] + #[serde(skip, default)] pub auth: Arc, pub host: String, pub user: String, @@ -29,8 +29,6 @@ impl Deref for ServiceSftp { fn deref(&self) -> &Self::Target { &self.auth } } -fn default_auth() -> Arc { Auth::new(AuthKind::Sftp, Scheme::Sftp, "") } - fn deserialize_path<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, diff --git a/yazi-config/src/vfs/vfs.rs b/yazi-config/src/vfs/vfs.rs index 7d936147..6ed4ca24 100644 --- a/yazi-config/src/vfs/vfs.rs +++ b/yazi-config/src/vfs/vfs.rs @@ -1,31 +1,33 @@ use std::io; use anyhow::{Context, Result}; -use serde::Deserialize; -use yazi_codegen::{DeserializeOver, DeserializeOver1}; +use serde::{Deserialize, Deserializer}; +use yazi_codegen::DeserializeOver; use yazi_fs::{Xdg, ok_or_not_found}; -use yazi_shared::auth::AuthInventory; +use yazi_shared::auth::{Auth, AuthInventory}; +use yazi_shim::toml::DeserializeOverWith; -use super::{Service, Services}; +use super::{Authorities, Service}; use crate::VFS; -#[derive(Deserialize, DeserializeOver, DeserializeOver1)] +#[derive(Deserialize, DeserializeOver)] pub struct Vfs { - pub services: Services, + #[serde(flatten)] + pub authorities: Authorities, } impl Vfs { - pub fn service

(domain: &str) -> io::Result

+ pub fn service

(auth: &Auth) -> io::Result

where P: TryFrom<&'static Service, Error = &'static str>, { - let Some((key, value)) = VFS.services.get_key_value(domain) else { - return Err(io::Error::other(format!("No such VFS service: {domain}"))); + let Some(value) = VFS.authorities.get(&auth.scheme, &auth.domain) else { + return Err(io::Error::other(format!("No such VFS service: {auth}"))); }; match value.try_into() { Ok(p) => Ok(p), - Err(e) => Err(io::Error::other(format!("VFS service `{key}` has wrong kind: {e}"))), + Err(e) => Err(io::Error::other(format!("VFS service `{auth}` has wrong kind: {e}"))), } } @@ -36,18 +38,17 @@ impl Vfs { } } +impl DeserializeOverWith for Vfs { + fn deserialize_over_with<'de, D: Deserializer<'de>>(self, de: D) -> Result { + Ok(Self { authorities: self.authorities.deserialize_over_with(de)? }) + } +} + // --- Inject inventory::submit! { AuthInventory { get: |scheme, domain| { - VFS.services.get(domain).and_then(|service| { - let auth = service.auth(); - if auth.scheme == scheme { - Some(auth.clone()) - } else { - None - } - }) + VFS.authorities.get(scheme, domain).map(|service| service.auth().clone()) }, } } diff --git a/yazi-shared/src/auth/auth.rs b/yazi-shared/src/auth/auth.rs index 3a6efe2f..d1027b76 100644 --- a/yazi-shared/src/auth/auth.rs +++ b/yazi-shared/src/auth/auth.rs @@ -11,10 +11,8 @@ pub(super) static DEFAULT_ARC: RoCell> = RoCell::new(); #[derive(Debug, Deserialize, Eq, Hash, PartialEq)] pub struct Auth { - #[serde(default)] pub kind: AuthKind, pub scheme: Scheme, - #[serde(default)] pub domain: SStr, } diff --git a/yazi-vfs/src/engine/lua/lua.rs b/yazi-vfs/src/engine/lua/lua.rs index cb4cdf65..e9810f63 100644 --- a/yazi-vfs/src/engine/lua/lua.rs +++ b/yazi-vfs/src/engine/lua/lua.rs @@ -110,11 +110,7 @@ impl<'a> Engine for Lua<'a> { )); }; - let service = Vfs::service::<&ServiceLua>(&auth.domain)?; - if service.scheme != auth.scheme { - return Err(io::Error::other(format!("No such custom VFS authority: {auth}"))); - } - + let service = Vfs::service::<&ServiceLua>(auth)?; Ok(Self::Me { url, run: &service.run }) } diff --git a/yazi-vfs/src/engine/sftp/sftp.rs b/yazi-vfs/src/engine/sftp/sftp.rs index 40820533..fca991df 100644 --- a/yazi-vfs/src/engine/sftp/sftp.rs +++ b/yazi-vfs/src/engine/sftp/sftp.rs @@ -155,7 +155,7 @@ impl<'a> Engine for Sftp<'a> { return Err(io::Error::new(io::ErrorKind::InvalidInput, format!("Not a SFTP URL: {url:?}"))); }; - let config = Vfs::service::<&ServiceSftp>(&auth.domain)?; + let config = Vfs::service::<&ServiceSftp>(auth)?; Ok(Self::Me { url, path: loc.as_inner(), config }) }