mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 14:51:03 +00:00
feat!: support VFS services with same name but different schemes (#4120)
This commit is contained in:
parent
ddc6632505
commit
d39637f40c
12 changed files with 138 additions and 82 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
[services]
|
||||
48
yazi-config/src/vfs/authorities.rs
Normal file
48
yazi-config/src/vfs/authorities.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
62
yazi-config/src/vfs/domains.rs
Normal file
62
yazi-config/src/vfs/domains.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(lua service services sftp vfs);
|
||||
yazi_macro::mod_flat!(authorities domains lua service sftp vfs);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue