feat!: support VFS services with same name but different schemes (#4120)

This commit is contained in:
三咲雅 misaki masa 2026-07-13 20:01:47 +08:00 committed by GitHub
parent ddc6632505
commit d39637f40c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 138 additions and 82 deletions

View file

@ -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 `<BackTab>` to `<S-Tab>` ([#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

View file

@ -1 +0,0 @@
[services]

View file

@ -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<Scheme, Domains>);
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<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
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<M: MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
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<Self, D::Error> {
for (scheme, domains) in Self::deserialize(de)?.0 {
self.0.entry(scheme).or_default().extend(domains.0);
}
Ok(self)
}
}

View file

@ -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<KebabCasedKey, Service>);
impl Deref for Domains {
type Target = HashMap<KebabCasedKey, Service>;
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<D: Deserializer<'de>>(self, deserializer: D) -> Result<Self::Value, D::Error> {
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::<KebabCasedKey, ServiceSftp>::deserialize(deserializer)?;
map.into_iter().map(|(domain, service)| (domain, Service::Sftp(service))).collect()
}
Scheme::Custom(_) => {
let map = HashMap::<KebabCasedKey, Service>::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)
}
}

View file

@ -5,7 +5,7 @@ use yazi_shared::{auth::Auth, event::Cmd};
#[derive(Deserialize)]
pub struct ServiceLua {
#[serde(flatten)]
#[serde(skip, default)]
pub auth: Arc<Auth>,
pub run: Cmd,
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(lua service services sftp vfs);
yazi_macro::mod_flat!(authorities domains lua service sftp vfs);

View file

@ -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<KebabCasedKey, Service>);
impl Deref for Services {
type Target = HashMap<KebabCasedKey, Service>;
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<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
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<Self, D::Error> {
Ok(Self(self.0.deserialize_over_with(de)?))
}
}
impl DeserializeOverHook for Services {
fn deserialize_over_hook(mut self) -> Result<Self, toml::de::Error> {
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)
}
}

View file

@ -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<Auth>,
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> { Auth::new(AuthKind::Sftp, Scheme::Sftp, "") }
fn deserialize_path<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
where
D: Deserializer<'de>,

View file

@ -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<P>(domain: &str) -> io::Result<P>
pub fn service<P>(auth: &Auth) -> io::Result<P>
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<Self, D::Error> {
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())
},
}
}

View file

@ -11,10 +11,8 @@ pub(super) static DEFAULT_ARC: RoCell<Arc<Auth>> = RoCell::new();
#[derive(Debug, Deserialize, Eq, Hash, PartialEq)]
pub struct Auth {
#[serde(default)]
pub kind: AuthKind,
pub scheme: Scheme,
#[serde(default)]
pub domain: SStr,
}

View file

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

View file

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