feat: new yazi-vfs crate (#3187)

This commit is contained in:
三咲雅 misaki masa 2025-09-21 11:44:52 +08:00 committed by GitHub
parent 30c0279570
commit 4e3ac54c8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
88 changed files with 930 additions and 286 deletions

22
Cargo.lock generated
View file

@ -4672,6 +4672,7 @@ dependencies = [
"yazi-fs", "yazi-fs",
"yazi-macro", "yazi-macro",
"yazi-shared", "yazi-shared",
"yazi-vfs",
] ]
[[package]] [[package]]
@ -4702,6 +4703,7 @@ dependencies = [
"yazi-fs", "yazi-fs",
"yazi-macro", "yazi-macro",
"yazi-shared", "yazi-shared",
"yazi-vfs",
] ]
[[package]] [[package]]
@ -4726,6 +4728,7 @@ dependencies = [
"ratatui", "ratatui",
"regex", "regex",
"serde", "serde",
"tokio",
"toml 0.9.6", "toml 0.9.6",
"tracing", "tracing",
"yazi-codegen", "yazi-codegen",
@ -4733,6 +4736,7 @@ dependencies = [
"yazi-macro", "yazi-macro",
"yazi-shared", "yazi-shared",
"yazi-term", "yazi-term",
"yazi-vfs",
] ]
[[package]] [[package]]
@ -4836,6 +4840,7 @@ dependencies = [
"yazi-proxy", "yazi-proxy",
"yazi-shared", "yazi-shared",
"yazi-term", "yazi-term",
"yazi-vfs",
"yazi-watcher", "yazi-watcher",
"yazi-widgets", "yazi-widgets",
] ]
@ -4857,6 +4862,7 @@ dependencies = [
"objc", "objc",
"parking_lot", "parking_lot",
"regex", "regex",
"russh",
"scopeguard", "scopeguard",
"serde", "serde",
"tokio", "tokio",
@ -4868,6 +4874,7 @@ dependencies = [
"yazi-macro", "yazi-macro",
"yazi-sftp", "yazi-sftp",
"yazi-shared", "yazi-shared",
"yazi-vfs",
] ]
[[package]] [[package]]
@ -5027,6 +5034,21 @@ dependencies = [
"yazi-shared", "yazi-shared",
] ]
[[package]]
name = "yazi-vfs"
version = "25.9.15"
dependencies = [
"anyhow",
"dirs",
"hashbrown 0.16.0",
"serde",
"tokio",
"toml 0.9.6",
"uzers",
"yazi-macro",
"yazi-shared",
]
[[package]] [[package]]
name = "yazi-watcher" name = "yazi-watcher"
version = "25.9.15" version = "25.9.15"

View file

@ -1,7 +1,7 @@
use std::{ffi::OsString, mem, path::MAIN_SEPARATOR_STR}; use std::{ffi::OsString, mem, path::MAIN_SEPARATOR_STR};
use anyhow::Result; use anyhow::Result;
use yazi_fs::{CWD, path::expand_url, provider}; use yazi_fs::{CWD, path::expand_url, provider::{self, DirReader, FileHolder}};
use yazi_macro::{act, render, succ}; use yazi_macro::{act, render, succ};
use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt}; use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt};
use yazi_proxy::CmpProxy; use yazi_proxy::CmpProxy;
@ -45,7 +45,7 @@ impl Actor for Trigger {
cache.push(CmpItem { name: OsString::new(), is_dir: true }); cache.push(CmpItem { name: OsString::new(), is_dir: true });
} }
while let Ok(Some(ent)) = dir.next_entry().await { while let Ok(Some(ent)) = dir.next().await {
if let Ok(ft) = ent.file_type().await { if let Ok(ft) = ent.file_type().await {
cache.push(CmpItem { name: ent.name().into_owned(), is_dir: ft.is_dir() }); cache.push(CmpItem { name: ent.name().into_owned(), is_dir: ft.is_dir() });
} }

View file

@ -49,7 +49,7 @@ impl Actor for BulkRename {
.write_all(old.join(OsStr::new("\n")).as_encoded_bytes()) .write_all(old.join(OsStr::new("\n")).as_encoded_bytes())
.await?; .await?;
defer! { tokio::spawn(Local::remove_file(tmp.clone())); } defer! { tokio::spawn(Local.remove_file(tmp.clone())); }
TasksProxy::process_exec(Cow::Borrowed(opener), cwd, vec![ TasksProxy::process_exec(Cow::Borrowed(opener), cwd, vec![
OsString::new(), OsString::new(),
tmp.to_owned().into(), tmp.to_owned().into(),
@ -60,7 +60,8 @@ impl Actor for BulkRename {
defer!(AppProxy::resume()); defer!(AppProxy::resume());
AppProxy::stop().await; AppProxy::stop().await;
let new: Vec<_> = Local::read_to_string(&tmp) let new: Vec<_> = Local
.read_to_string(&tmp)
.await? .await?
.lines() .lines()
.take(old.len()) .take(old.len())

View file

@ -39,7 +39,7 @@ impl Image {
}) })
.await??; .await??;
Ok(Local::write(cache, buf).await?) Ok(Local.write(cache, buf).await?)
} }
pub(super) async fn downscale(path: &Path, rect: Rect) -> Result<DynamicImage> { pub(super) async fn downscale(path: &Path, rect: Rect) -> Result<DynamicImage> {

View file

@ -14,6 +14,7 @@ yazi-config = { path = "../yazi-config", version = "25.9.15" }
yazi-fs = { path = "../yazi-fs", version = "25.9.15" } yazi-fs = { path = "../yazi-fs", version = "25.9.15" }
yazi-macro = { path = "../yazi-macro", version = "25.9.15" } yazi-macro = { path = "../yazi-macro", version = "25.9.15" }
yazi-shared = { path = "../yazi-shared", version = "25.9.15" } yazi-shared = { path = "../yazi-shared", version = "25.9.15" }
yazi-vfs = { path = "../yazi-vfs", version = "25.9.15" }
# External dependencies # External dependencies
clap = { workspace = true } clap = { workspace = true }

View file

@ -1,5 +1,5 @@
use yazi_config::YAZI; use yazi_config::YAZI;
use yazi_fs::Xdg; use yazi_vfs::local::Xdg;
use super::Actions; use super::Actions;

View file

@ -3,8 +3,9 @@ use std::path::PathBuf;
use futures::executor::block_on; use futures::executor::block_on;
use hashbrown::HashSet; use hashbrown::HashSet;
use serde::Serialize; use serde::Serialize;
use yazi_fs::{CWD, Xdg, path::expand_url, provider}; use yazi_fs::{CWD, path::expand_url, provider};
use yazi_shared::url::{UrlBuf, UrnBuf}; use yazi_shared::url::{UrlBuf, UrnBuf};
use yazi_vfs::local::Xdg;
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Serialize)]
pub struct Boot { pub struct Boot {

View file

@ -24,6 +24,7 @@ yazi-dds = { path = "../yazi-dds", version = "25.9.15" }
yazi-fs = { path = "../yazi-fs", version = "25.9.15" } yazi-fs = { path = "../yazi-fs", version = "25.9.15" }
yazi-macro = { path = "../yazi-macro", version = "25.9.15" } yazi-macro = { path = "../yazi-macro", version = "25.9.15" }
yazi-shared = { path = "../yazi-shared", version = "25.9.15" } yazi-shared = { path = "../yazi-shared", version = "25.9.15" }
yazi-vfs = { path = "../yazi-vfs", version = "25.9.15" }
# External dependencies # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }

View file

@ -23,7 +23,7 @@ impl Dependency {
pub(super) async fn delete_assets(&self) -> Result<()> { pub(super) async fn delete_assets(&self) -> Result<()> {
let assets = self.target().join("assets"); let assets = self.target().join("assets");
match Local::read_dir(&assets).await { match Local.read_dir(&assets).await {
Ok(mut it) => { Ok(mut it) => {
while let Some(entry) = it.next().await? { while let Some(entry) = it.next().await? {
remove_sealed(&entry.path()) remove_sealed(&entry.path())
@ -49,7 +49,7 @@ impl Dependency {
.with_context(|| format!("failed to delete `{}`", path.display()))?; .with_context(|| format!("failed to delete `{}`", path.display()))?;
} }
if ok_or_not_found(Local::remove_dir(&dir).await).is_ok() { if ok_or_not_found(Local.remove_dir(&dir).await).is_ok() {
outln!("Done!")?; outln!("Done!")?;
} else { } else {
outln!( outln!(

View file

@ -3,8 +3,9 @@ use std::{io::BufWriter, path::{Path, PathBuf}, str::FromStr};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use twox_hash::XxHash3_128; use twox_hash::XxHash3_128;
use yazi_fs::{Xdg, provider::{DirReader, FileHolder, Provider, local::Local}}; use yazi_fs::provider::{DirReader, FileHolder, Provider, local::Local};
use yazi_shared::BytesExt; use yazi_shared::BytesExt;
use yazi_vfs::local::Xdg;
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub(crate) struct Dependency { pub(crate) struct Dependency {
@ -62,7 +63,7 @@ impl Dependency {
} }
pub(super) async fn plugin_files(dir: &Path) -> std::io::Result<Vec<String>> { pub(super) async fn plugin_files(dir: &Path) -> std::io::Result<Vec<String>> {
let mut it = Local::read_dir(dir).await?; let mut it = Local.read_dir(dir).await?;
let mut files: Vec<String> = let mut files: Vec<String> =
["LICENSE", "README.md", "main.lua"].into_iter().map(Into::into).collect(); ["LICENSE", "README.md", "main.lua"].into_iter().map(Into::into).collect();
while let Some(entry) = it.next().await? { while let Some(entry) = it.next().await? {

View file

@ -20,7 +20,7 @@ impl Dependency {
self.hash_check().await?; self.hash_check().await?;
} }
Local::create_dir_all(&to).await?; Local.create_dir_all(&to).await?;
self.delete_assets().await?; self.delete_assets().await?;
let res1 = Self::deploy_assets(from.join("assets"), to.join("assets")).await; let res1 = Self::deploy_assets(from.join("assets"), to.join("assets")).await;
@ -40,9 +40,9 @@ impl Dependency {
} }
async fn deploy_assets(from: PathBuf, to: PathBuf) -> Result<()> { async fn deploy_assets(from: PathBuf, to: PathBuf) -> Result<()> {
match Local::read_dir(&from).await { match Local.read_dir(&from).await {
Ok(mut it) => { Ok(mut it) => {
Local::create_dir_all(&to).await?; Local.create_dir_all(&to).await?;
while let Some(entry) = it.next().await? { while let Some(entry) = it.next().await? {
let (src, dist) = (entry.path(), to.join(entry.name())); let (src, dist) = (entry.path(), to.join(entry.name()));
copy_and_seal(&src, &dist).await.with_context(|| { copy_and_seal(&src, &dist).await.with_context(|| {

View file

@ -14,17 +14,17 @@ impl Dependency {
for file in files { for file in files {
h.write(file.as_bytes()); h.write(file.as_bytes());
h.write(b"VpvFw9Atb7cWGOdqhZCra634CcJJRlsRl72RbZeV0vpG1\0"); h.write(b"VpvFw9Atb7cWGOdqhZCra634CcJJRlsRl72RbZeV0vpG1\0");
h.write(&ok_or_not_found(Local::read(dir.join(file)).await)?); h.write(&ok_or_not_found(Local.read(dir.join(file)).await)?);
} }
let mut assets = vec![]; let mut assets = vec![];
match Local::read_dir(dir.join("assets")).await { match Local.read_dir(dir.join("assets")).await {
Ok(mut it) => { Ok(mut it) => {
while let Some(entry) = it.next().await? { while let Some(entry) = it.next().await? {
let Ok(name) = entry.name().into_owned().into_string() else { let Ok(name) = entry.name().into_owned().into_string() else {
bail!("asset path is not valid UTF-8: {}", entry.path().display()); bail!("asset path is not valid UTF-8: {}", entry.path().display());
}; };
assets.push((name, Local::read(entry.path()).await?)); assets.push((name, Local.read(entry.path()).await?));
} }
} }
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}

View file

@ -3,7 +3,7 @@
yazi_macro::mod_flat!(add delete dependency deploy git hash install package upgrade); yazi_macro::mod_flat!(add delete dependency deploy git hash install package upgrade);
use anyhow::Context; use anyhow::Context;
use yazi_fs::Xdg; use yazi_vfs::local::Xdg;
pub(super) fn init() -> anyhow::Result<()> { pub(super) fn init() -> anyhow::Result<()> {
let root = Xdg::state_dir().join("packages"); let root = Xdg::state_dir().join("packages");

View file

@ -2,8 +2,9 @@ use std::{path::PathBuf, str::FromStr};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use yazi_fs::{Xdg, provider::{Provider, local::Local}}; use yazi_fs::provider::{Provider, local::Local};
use yazi_macro::outln; use yazi_macro::outln;
use yazi_vfs::local::Xdg;
use super::Dependency; use super::Dependency;
@ -15,7 +16,7 @@ pub(crate) struct Package {
impl Package { impl Package {
pub(crate) async fn load() -> Result<Self> { pub(crate) async fn load() -> Result<Self> {
Ok(match Local::read_to_string(Self::toml()).await { Ok(match Local.read_to_string(Self::toml()).await {
Ok(s) => toml::from_str(&s)?, Ok(s) => toml::from_str(&s)?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Self::default(), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Self::default(),
Err(e) => Err(e)?, Err(e) => Err(e)?,
@ -135,7 +136,7 @@ impl Package {
async fn save(&self) -> Result<()> { async fn save(&self) -> Result<()> {
let s = toml::to_string_pretty(self)?; let s = toml::to_string_pretty(self)?;
Local::write(Self::toml(), s).await.context("Failed to write package.toml") Local.write(Self::toml(), s).await.context("Failed to write package.toml")
} }
fn toml() -> PathBuf { Xdg::config_dir().join("package.toml") } fn toml() -> PathBuf { Xdg::config_dir().join("package.toml") }

View file

@ -5,19 +5,19 @@ use yazi_fs::{ok_or_not_found, provider::{FileBuilder, Provider, local::{Gate, L
#[inline] #[inline]
pub async fn must_exists(path: impl AsRef<Path>) -> bool { pub async fn must_exists(path: impl AsRef<Path>) -> bool {
Local::symlink_metadata(path).await.is_ok() Local.symlink_metadata(path).await.is_ok()
} }
#[inline] #[inline]
pub async fn maybe_exists(path: impl AsRef<Path>) -> bool { pub async fn maybe_exists(path: impl AsRef<Path>) -> bool {
match Local::symlink_metadata(path).await { match Local.symlink_metadata(path).await {
Ok(_) => true, Ok(_) => true,
Err(e) => e.kind() != std::io::ErrorKind::NotFound, Err(e) => e.kind() != std::io::ErrorKind::NotFound,
} }
} }
pub async fn copy_and_seal(from: &Path, to: &Path) -> io::Result<()> { pub async fn copy_and_seal(from: &Path, to: &Path) -> io::Result<()> {
let b = Local::read(from).await?; let b = Local.read(from).await?;
ok_or_not_found(remove_sealed(to).await)?; ok_or_not_found(remove_sealed(to).await)?;
let mut file = Gate::default().create_new(true).write(true).truncate(true).open(to).await?; let mut file = Gate::default().create_new(true).write(true).truncate(true).open(to).await?;
@ -39,5 +39,5 @@ pub async fn remove_sealed(p: &Path) -> io::Result<()> {
tokio::fs::set_permissions(p, perm).await?; tokio::fs::set_permissions(p, perm).await?;
} }
Local::remove_file(p).await Local.remove_file(p).await
} }

View file

@ -14,6 +14,7 @@ yazi-fs = { path = "../yazi-fs", version = "25.9.15" }
yazi-macro = { path = "../yazi-macro", version = "25.9.15" } yazi-macro = { path = "../yazi-macro", version = "25.9.15" }
yazi-shared = { path = "../yazi-shared", version = "25.9.15" } yazi-shared = { path = "../yazi-shared", version = "25.9.15" }
yazi-term = { path = "../yazi-term", version = "25.9.15" } yazi-term = { path = "../yazi-term", version = "25.9.15" }
yazi-vfs = { path = "../yazi-vfs", version = "25.9.15" }
# External dependencies # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
@ -26,6 +27,7 @@ indexmap = { workspace = true }
ratatui = { workspace = true } ratatui = { workspace = true }
regex = { workspace = true } regex = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
tokio = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }

View file

@ -1,8 +1,9 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use serde::Deserialize; use serde::Deserialize;
use yazi_codegen::DeserializeOver1; use yazi_codegen::DeserializeOver1;
use yazi_fs::{Xdg, ok_or_not_found}; use yazi_fs::ok_or_not_found;
use yazi_shared::Layer; use yazi_shared::Layer;
use yazi_vfs::local::Xdg;
use super::{Chord, KeymapRules}; use super::{Chord, KeymapRules};

View file

@ -1,6 +1,6 @@
#![allow(clippy::module_inception)] #![allow(clippy::module_inception)]
yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview tasks theme vfs which); yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview tasks theme which);
yazi_macro::mod_flat!(color icon layout pattern platform preset priority style yazi); yazi_macro::mod_flat!(color icon layout pattern platform preset priority style yazi);

View file

@ -3,8 +3,9 @@ use std::{borrow::Cow, path::PathBuf};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use yazi_codegen::DeserializeOver2; use yazi_codegen::DeserializeOver2;
use yazi_fs::{Xdg, path::expand_url}; use yazi_fs::path::expand_url;
use yazi_shared::{SStr, timestamp_us, url::Url}; use yazi_shared::{SStr, timestamp_us, url::Url};
use yazi_vfs::local::Xdg;
use super::PreviewWrap; use super::PreviewWrap;

View file

@ -4,7 +4,7 @@ use anyhow::{Context, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use toml::Value; use toml::Value;
use yazi_codegen::DeserializeOver2; use yazi_codegen::DeserializeOver2;
use yazi_fs::Xdg; use yazi_vfs::local::Xdg;
#[derive(Default, Deserialize, DeserializeOver2, Serialize)] #[derive(Default, Deserialize, DeserializeOver2, Serialize)]
pub struct Flavor { pub struct Flavor {

View file

@ -3,8 +3,9 @@ use std::path::PathBuf;
use anyhow::{Context, Result, anyhow, bail}; use anyhow::{Context, Result, anyhow, bail};
use serde::Deserialize; use serde::Deserialize;
use yazi_codegen::{DeserializeOver1, DeserializeOver2}; use yazi_codegen::{DeserializeOver1, DeserializeOver2};
use yazi_fs::{Xdg, ok_or_not_found, path::expand_url}; use yazi_fs::{ok_or_not_found, path::expand_url};
use yazi_shared::url::UrlBuf; use yazi_shared::url::UrlBuf;
use yazi_vfs::local::Xdg;
use super::{Filetype, Flavor, Icon}; use super::{Filetype, Flavor, Icon};
use crate::Style; use crate::Style;

View file

@ -1 +0,0 @@
yazi_macro::mod_flat!(sftp);

View file

@ -1,9 +0,0 @@
use std::path::PathBuf;
pub struct Sftp {
pub host: String,
pub user: String,
pub port: u16,
pub password: Option<String>,
pub key_file: Option<PathBuf>,
}

View file

@ -1,7 +1,8 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use serde::Deserialize; use serde::Deserialize;
use yazi_codegen::DeserializeOver1; use yazi_codegen::DeserializeOver1;
use yazi_fs::{Xdg, ok_or_not_found}; use yazi_fs::ok_or_not_found;
use yazi_vfs::local::Xdg;
use crate::{mgr, open, opener, plugin, popup, preview, tasks, which}; use crate::{mgr, open, opener, plugin, popup, preview, tasks, which};

View file

@ -24,7 +24,7 @@ pub struct Client {
pub(super) abilities: HashSet<String>, pub(super) abilities: HashSet<String>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Peer { pub struct Peer {
pub(super) abilities: HashSet<String>, pub(super) abilities: HashSet<String>,
} }

View file

@ -7,7 +7,7 @@ use yazi_shared::url::UrlBuf;
use super::Ember; use super::Ember;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct EmberBulk<'a> { pub struct EmberBulk<'a> {
pub changes: HashMap<Cow<'a, UrlBuf>, Cow<'a, UrlBuf>>, pub changes: HashMap<Cow<'a, UrlBuf>, Cow<'a, UrlBuf>>,
} }

View file

@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use super::Ember; use super::Ember;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct EmberBye; pub struct EmberBye;
impl EmberBye { impl EmberBye {

View file

@ -6,7 +6,7 @@ use yazi_shared::{Id, url::UrlBuf};
use super::Ember; use super::Ember;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct EmberCd<'a> { pub struct EmberCd<'a> {
pub tab: Id, pub tab: Id,
pub url: Cow<'a, UrlBuf>, pub url: Cow<'a, UrlBuf>,

View file

@ -6,7 +6,7 @@ use yazi_shared::url::UrlBuf;
use super::Ember; use super::Ember;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct EmberDelete<'a> { pub struct EmberDelete<'a> {
pub urls: Cow<'a, Vec<UrlBuf>>, pub urls: Cow<'a, Vec<UrlBuf>>,
} }

View file

@ -7,7 +7,7 @@ use super::{Ember, EmberHi};
use crate::Peer; use crate::Peer;
/// Server handshake /// Server handshake
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct EmberHey { pub struct EmberHey {
pub peers: HashMap<Id, Peer>, pub peers: HashMap<Id, Peer>,
pub version: SStr, pub version: SStr,

View file

@ -8,7 +8,7 @@ use yazi_shared::SStr;
use super::Ember; use super::Ember;
/// Client handshake /// Client handshake
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct EmberHi<'a> { pub struct EmberHi<'a> {
/// Kinds of events the client can handle /// Kinds of events the client can handle
pub abilities: HashSet<Cow<'a, str>>, pub abilities: HashSet<Cow<'a, str>>,

View file

@ -6,7 +6,7 @@ use yazi_shared::{Id, url::UrlBuf};
use super::Ember; use super::Ember;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct EmberHover<'a> { pub struct EmberHover<'a> {
pub tab: Id, pub tab: Id,
pub url: Option<Cow<'a, UrlBuf>>, pub url: Option<Cow<'a, UrlBuf>>,

View file

@ -7,7 +7,7 @@ use yazi_shared::{Id, url::UrlBuf};
use super::Ember; use super::Ember;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct EmberLoad<'a> { pub struct EmberLoad<'a> {
pub tab: Id, pub tab: Id,
pub url: Cow<'a, UrlBuf>, pub url: Cow<'a, UrlBuf>,

View file

@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use super::Ember; use super::Ember;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct EmberMount; pub struct EmberMount;
impl EmberMount { impl EmberMount {

View file

@ -6,7 +6,7 @@ use yazi_shared::url::UrlBuf;
use super::Ember; use super::Ember;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct EmberMove<'a> { pub struct EmberMove<'a> {
pub items: Cow<'a, Vec<BodyMoveItem>>, pub items: Cow<'a, Vec<BodyMoveItem>>,
} }
@ -34,7 +34,7 @@ impl IntoLua for EmberMove<'_> {
} }
// --- Item // --- Item
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct BodyMoveItem { pub struct BodyMoveItem {
pub from: UrlBuf, pub from: UrlBuf,
pub to: UrlBuf, pub to: UrlBuf,

View file

@ -6,7 +6,7 @@ use yazi_shared::{Id, url::UrlBuf};
use super::Ember; use super::Ember;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct EmberRename<'a> { pub struct EmberRename<'a> {
pub tab: Id, pub tab: Id,
pub from: Cow<'a, UrlBuf>, pub from: Cow<'a, UrlBuf>,

View file

@ -4,7 +4,7 @@ use yazi_shared::Id;
use super::Ember; use super::Ember;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct EmberTab { pub struct EmberTab {
pub id: Id, pub id: Id,
} }

View file

@ -6,7 +6,7 @@ use yazi_shared::url::UrlBuf;
use super::Ember; use super::Ember;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct EmberTrash<'a> { pub struct EmberTrash<'a> {
pub urls: Cow<'a, Vec<UrlBuf>>, pub urls: Cow<'a, Vec<UrlBuf>>,
} }

View file

@ -8,7 +8,7 @@ use yazi_shared::url::UrlBufCov;
use super::Ember; use super::Ember;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct EmberYank<'a>(UpdateYankedOpt<'a>); pub struct EmberYank<'a>(UpdateYankedOpt<'a>);
impl<'a> EmberYank<'a> { impl<'a> EmberYank<'a> {

View file

@ -58,7 +58,7 @@ impl State {
return Ok(()); return Ok(());
} }
Local::create_dir_all(&BOOT.state_dir).await?; Local.create_dir_all(&BOOT.state_dir).await?;
let mut buf = BufWriter::new( let mut buf = BufWriter::new(
Gate::default() Gate::default()
.write(true) .write(true)
@ -79,7 +79,7 @@ impl State {
} }
async fn load(&self) -> Result<()> { async fn load(&self) -> Result<()> {
let mut file = BufReader::new(Local::open(BOOT.state_dir.join(".dds")).await?); let mut file = BufReader::new(Local.open(BOOT.state_dir.join(".dds")).await?);
let mut buf = String::new(); let mut buf = String::new();
let mut inner = HashMap::new(); let mut inner = HashMap::new();
@ -103,7 +103,7 @@ impl State {
} }
async fn skip(&self) -> Result<bool> { async fn skip(&self) -> Result<bool> {
let cha = Local::symlink_metadata(BOOT.state_dir.join(".dds")).await?; let cha = Local.symlink_metadata(BOOT.state_dir.join(".dds")).await?;
let modified = cha.mtime_dur()?.as_micros(); let modified = cha.mtime_dur()?.as_micros();
Ok(modified >= self.last.load(Ordering::Relaxed) as u128) Ok(modified >= self.last.load(Ordering::Relaxed) as u128)
} }

View file

@ -40,7 +40,7 @@ impl Stream {
let p = Self::socket_file(); let p = Self::socket_file();
yazi_fs::provider::local::Local::remove_file(&p).await.ok(); yazi_fs::provider::local::Local.remove_file(&p).await.ok();
tokio::net::UnixListener::bind(p) tokio::net::UnixListener::bind(p)
} }

View file

@ -37,6 +37,7 @@ yazi-plugin = { path = "../yazi-plugin", version = "25.9.15" }
yazi-proxy = { path = "../yazi-proxy", version = "25.9.15" } yazi-proxy = { path = "../yazi-proxy", version = "25.9.15" }
yazi-shared = { path = "../yazi-shared", version = "25.9.15" } yazi-shared = { path = "../yazi-shared", version = "25.9.15" }
yazi-term = { path = "../yazi-term", version = "25.9.15" } yazi-term = { path = "../yazi-term", version = "25.9.15" }
yazi-vfs = { path = "../yazi-vfs", version = "25.9.15" }
yazi-watcher = { path = "../yazi-watcher", version = "25.9.15" } yazi-watcher = { path = "../yazi-watcher", version = "25.9.15" }
yazi-widgets = { path = "../yazi-widgets", version = "25.9.15" } yazi-widgets = { path = "../yazi-widgets", version = "25.9.15" }

View file

@ -26,13 +26,13 @@ impl App {
async fn cwd_to_file(&self, no: bool) { async fn cwd_to_file(&self, no: bool) {
if let Some(p) = ARGS.cwd_file.as_ref().filter(|_| !no) { if let Some(p) = ARGS.cwd_file.as_ref().filter(|_| !no) {
let cwd = self.core.mgr.cwd().os_str(); let cwd = self.core.mgr.cwd().os_str();
Local::write(p, cwd.as_encoded_bytes()).await.ok(); Local.write(p, cwd.as_encoded_bytes()).await.ok();
} }
} }
async fn selected_to_file(&self, selected: Option<OsString>) { async fn selected_to_file(&self, selected: Option<OsString>) {
if let (Some(s), Some(p)) = (selected, &ARGS.chooser_file) { if let (Some(s), Some(p)) = (selected, &ARGS.chooser_file) {
Local::write(p, s.as_encoded_bytes()).await.ok(); Local.write(p, s.as_encoded_bytes()).await.ok();
} }
} }
} }

View file

@ -4,8 +4,8 @@ use anyhow::Context;
use crossterm::style::{Color, Print, ResetColor, SetForegroundColor}; use crossterm::style::{Color, Print, ResetColor, SetForegroundColor};
use tracing_appender::non_blocking::WorkerGuard; use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use yazi_fs::Xdg;
use yazi_shared::{LOG_LEVEL, RoCell}; use yazi_shared::{LOG_LEVEL, RoCell};
use yazi_vfs::local::Xdg;
static _GUARD: RoCell<WorkerGuard> = RoCell::new(); static _GUARD: RoCell<WorkerGuard> = RoCell::new();

View file

@ -13,6 +13,7 @@ yazi-ffi = { path = "../yazi-ffi", version = "25.9.15" }
yazi-macro = { path = "../yazi-macro", version = "25.9.15" } yazi-macro = { path = "../yazi-macro", version = "25.9.15" }
yazi-sftp = { path = "../yazi-sftp", version = "0.1.0" } yazi-sftp = { path = "../yazi-sftp", version = "0.1.0" }
yazi-shared = { path = "../yazi-shared", version = "25.9.15" } yazi-shared = { path = "../yazi-shared", version = "25.9.15" }
yazi-vfs = { path = "../yazi-vfs", version = "25.9.15" }
# External dependencies # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
@ -25,6 +26,7 @@ futures = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
parking_lot = { workspace = true } parking_lot = { workspace = true }
regex = { workspace = true } regex = { workspace = true }
russh = { workspace = true }
scopeguard = { workspace = true } scopeguard = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }

View file

@ -5,7 +5,7 @@ use tokio::{select, sync::mpsc::{self, UnboundedReceiver}};
use yazi_shared::{Id, url::{UrlBuf, Urn, UrnBuf}}; use yazi_shared::{Id, url::{UrlBuf, Urn, UrnBuf}};
use super::{FilesSorter, Filter}; use super::{FilesSorter, Filter};
use crate::{FILES_TICKET, File, FilesOp, SortBy, cha::Cha, mounts::PARTITIONS, provider::{self, DirEntry}}; use crate::{FILES_TICKET, File, FilesOp, SortBy, cha::Cha, mounts::PARTITIONS, provider::{self, DirEntry, DirReader, FileHolder}};
#[derive(Default)] #[derive(Default)]
pub struct Files { pub struct Files {
@ -40,7 +40,7 @@ impl Files {
let (tx, rx) = mpsc::unbounded_channel(); let (tx, rx) = mpsc::unbounded_channel();
tokio::spawn(async move { tokio::spawn(async move {
while let Ok(Some(ent)) = it.next_entry().await { while let Ok(Some(ent)) = it.next().await {
select! { select! {
_ = tx.closed() => break, _ = tx.closed() => break,
result = ent.metadata() => { result = ent.metadata() => {
@ -59,7 +59,7 @@ impl Files {
pub async fn from_dir_bulk(dir: &UrlBuf) -> std::io::Result<Vec<File>> { pub async fn from_dir_bulk(dir: &UrlBuf) -> std::io::Result<Vec<File>> {
let mut it = provider::read_dir(dir).await?; let mut it = provider::read_dir(dir).await?;
let mut entries = Vec::with_capacity(5000); let mut entries = Vec::with_capacity(5000);
while let Ok(Some(entry)) = it.next_entry().await { while let Ok(Some(entry)) = it.next().await {
entries.push(entry); entries.push(entry);
} }

View file

@ -2,7 +2,7 @@ use anyhow::Result;
use tokio::{io, select, sync::{mpsc, oneshot}, time}; use tokio::{io, select, sync::{mpsc, oneshot}, time};
use yazi_shared::url::{Component, Url, UrlBuf}; use yazi_shared::url::{Component, Url, UrlBuf};
use crate::{cha::Cha, provider}; use crate::{cha::Cha, provider::{self, DirReader, FileHolder}};
#[inline] #[inline]
pub async fn maybe_exists<'a>(url: impl Into<Url<'a>>) -> bool { pub async fn maybe_exists<'a>(url: impl Into<Url<'a>>) -> bool {
@ -83,7 +83,7 @@ pub fn copy_with_progress(
pub async fn remove_dir_clean(dir: &UrlBuf) { pub async fn remove_dir_clean(dir: &UrlBuf) {
let Ok(mut it) = provider::read_dir(dir).await else { return }; let Ok(mut it) = provider::read_dir(dir).await else { return };
while let Ok(Some(ent)) = it.next_entry().await { while let Ok(Some(ent)) = it.next().await {
if ent.file_type().await.is_ok_and(|t| t.is_dir()) { if ent.file_type().await.is_ok_and(|t| t.is_dir()) {
let url = ent.url(); let url = ent.url();
Box::pin(remove_dir_clean(&url)).await; Box::pin(remove_dir_clean(&url)).await;

View file

@ -2,7 +2,7 @@
yazi_macro::mod_pub!(cha mounts provider path); yazi_macro::mod_pub!(cha mounts provider path);
yazi_macro::mod_flat!(cwd file files filter fns op sorter sorting stage xdg); yazi_macro::mod_flat!(cwd file files filter fns op sorter sorting stage);
pub fn init() { pub fn init() {
CWD.init(<_>::default()); CWD.init(<_>::default());

View file

@ -2,7 +2,7 @@ use std::{collections::VecDeque, io, time::{Duration, Instant}};
use yazi_shared::{Either, url::{Url, UrlBuf}}; use yazi_shared::{Either, url::{Url, UrlBuf}};
use crate::provider::{self, ReadDir}; use crate::provider::{self, DirReader, FileHolder, ReadDir};
pub enum SizeCalculator { pub enum SizeCalculator {
File(Option<u64>), File(Option<u64>),
@ -65,7 +65,7 @@ impl SizeCalculator {
}; };
} }
let Ok(Some(ent)) = front.right_mut()?.next_entry().await else { let Ok(Some(ent)) = front.right_mut()?.next().await else {
pop_and_continue!(); pop_and_continue!();
}; };

View file

@ -1,41 +1,56 @@
use std::{borrow::Cow, ffi::OsStr, io}; use std::{borrow::Cow, ffi::OsStr, io, sync::Arc};
use yazi_shared::url::UrlBuf; use yazi_shared::url::UrlBuf;
use crate::{cha::{Cha, ChaType}, provider::FileHolder}; use crate::{cha::{Cha, ChaType}, provider::FileHolder};
pub enum DirEntry { pub enum DirEntry {
Local(super::local::DirEntry), Regular(super::local::DirEntry),
Search((Arc<UrlBuf>, super::local::DirEntry)),
Sftp((Arc<UrlBuf>, super::sftp::DirEntry)),
} }
impl From<super::local::DirEntry> for DirEntry { impl FileHolder for DirEntry {
fn from(value: super::local::DirEntry) -> Self { Self::Local(value) } fn path(&self) -> std::path::PathBuf {
match self {
Self::Regular(ent) => ent.path(),
Self::Search((_, ent)) => ent.path(),
Self::Sftp((_, ent)) => ent.path(),
}
}
fn name(&self) -> Cow<'_, OsStr> {
match self {
Self::Regular(ent) => ent.name(),
Self::Search((_, ent)) => ent.name(),
Self::Sftp((_, ent)) => ent.name(),
}
}
async fn metadata(&self) -> io::Result<Cha> {
match self {
Self::Regular(ent) => ent.metadata().await,
Self::Search((_, ent)) => ent.metadata().await,
Self::Sftp((_, ent)) => ent.metadata().await,
}
}
async fn file_type(&self) -> io::Result<ChaType> {
match self {
Self::Regular(ent) => ent.file_type().await,
Self::Search((_, ent)) => ent.file_type().await,
Self::Sftp((_, ent)) => ent.file_type().await,
}
}
} }
impl DirEntry { impl DirEntry {
#[must_use] #[must_use]
pub fn url(&self) -> UrlBuf { pub fn url(&self) -> UrlBuf {
match self { match self {
Self::Local(local) => local.path().into(), Self::Regular(ent) => ent.path().into(),
} Self::Search((dir, ent)) => dir.join(ent.name()),
} Self::Sftp((dir, ent)) => dir.join(ent.name()),
#[must_use]
pub fn name(&self) -> Cow<'_, OsStr> {
match self {
Self::Local(local) => local.name(),
}
}
pub async fn metadata(&self) -> io::Result<Cha> {
match self {
Self::Local(local) => local.metadata().await,
}
}
pub async fn file_type(&self) -> io::Result<ChaType> {
match self {
Self::Local(local) => local.file_type().await,
} }
} }
} }

View file

@ -0,0 +1,90 @@
use std::{io, path::Path};
use yazi_shared::scheme::SchemeRef;
use crate::provider::FileBuilder;
pub enum Gate {
Local(super::local::Gate),
Sftp(super::sftp::Gate),
}
impl From<super::local::Gate> for Gate {
fn from(value: super::local::Gate) -> Self { Self::Local(value) }
}
impl From<super::sftp::Gate> for Gate {
fn from(value: super::sftp::Gate) -> Self { Self::Sftp(value) }
}
impl FileBuilder for Gate {
type File = super::RwFile;
fn append(&mut self, append: bool) -> &mut Self {
match self {
Self::Local(g) => _ = g.append(append),
Self::Sftp(g) => _ = g.append(append),
};
self
}
fn create(&mut self, create: bool) -> &mut Self {
match self {
Self::Local(g) => _ = g.create(create),
Self::Sftp(g) => _ = g.create(create),
};
self
}
fn create_new(&mut self, create_new: bool) -> &mut Self {
match self {
Self::Local(g) => _ = g.create_new(create_new),
Self::Sftp(g) => _ = g.create_new(create_new),
};
self
}
async fn new(scheme: SchemeRef<'_>) -> io::Result<Self> {
Ok(match scheme {
SchemeRef::Regular | SchemeRef::Search(_) => super::local::Gate::new(scheme).await?.into(),
SchemeRef::Archive(_) => {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))?
}
SchemeRef::Sftp(_) => super::sftp::Gate::new(scheme).await?.into(),
})
}
async fn open<P>(&self, path: P) -> io::Result<Self::File>
where
P: AsRef<Path>,
{
Ok(match self {
Gate::Local(g) => g.open(path).await?.into(),
Gate::Sftp(g) => g.open(path).await?.into(),
})
}
fn read(&mut self, read: bool) -> &mut Self {
match self {
Self::Local(g) => _ = g.read(read),
Self::Sftp(g) => _ = g.read(read),
};
self
}
fn truncate(&mut self, truncate: bool) -> &mut Self {
match self {
Self::Local(g) => _ = g.truncate(truncate),
Self::Sftp(g) => _ = g.truncate(truncate),
};
self
}
fn write(&mut self, write: bool) -> &mut Self {
match self {
Self::Local(g) => _ = g.write(write),
Self::Sftp(g) => _ = g.write(write),
};
self
}
}

View file

@ -1,5 +1,7 @@
use std::{io, path::Path}; use std::{io, path::Path};
use yazi_shared::scheme::SchemeRef;
use crate::provider::FileBuilder; use crate::provider::FileBuilder;
#[derive(Default)] #[derive(Default)]
@ -23,6 +25,14 @@ impl FileBuilder for Gate {
self self
} }
async fn new(scheme: SchemeRef<'_>) -> io::Result<Self> {
if scheme.is_virtual() {
Err(io::Error::new(io::ErrorKind::InvalidInput, "Not a local filesystem"))?
} else {
Ok(Self::default())
}
}
async fn open<P>(&self, path: P) -> io::Result<Self::File> async fn open<P>(&self, path: P) -> io::Result<Self::File>
where where
P: AsRef<Path>, P: AsRef<Path>,

View file

@ -2,6 +2,7 @@ use std::{io, path::{Path, PathBuf}};
use crate::{cha::Cha, provider::Provider}; use crate::{cha::Cha, provider::Provider};
#[derive(Clone, Copy)]
pub struct Local; pub struct Local;
impl Provider for Local { impl Provider for Local {
@ -10,7 +11,7 @@ impl Provider for Local {
type ReadDir = super::ReadDir; type ReadDir = super::ReadDir;
#[inline] #[inline]
fn cache<P>(_: P) -> Option<PathBuf> fn cache<P>(&self, _: P) -> Option<PathBuf>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -18,7 +19,7 @@ impl Provider for Local {
} }
#[inline] #[inline]
async fn canonicalize<P>(path: P) -> io::Result<PathBuf> async fn canonicalize<P>(&self, path: P) -> io::Result<PathBuf>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -26,7 +27,7 @@ impl Provider for Local {
} }
#[inline] #[inline]
async fn copy<P, Q>(from: P, to: Q, cha: Cha) -> io::Result<u64> async fn copy<P, Q>(&self, from: P, to: Q, cha: Cha) -> io::Result<u64>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
@ -37,7 +38,7 @@ impl Provider for Local {
} }
#[inline] #[inline]
async fn create_dir<P>(path: P) -> io::Result<()> async fn create_dir<P>(&self, path: P) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -45,7 +46,7 @@ impl Provider for Local {
} }
#[inline] #[inline]
async fn create_dir_all<P>(path: P) -> io::Result<()> async fn create_dir_all<P>(&self, path: P) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -53,7 +54,10 @@ impl Provider for Local {
} }
#[inline] #[inline]
async fn hard_link<P, Q>(original: P, link: Q) -> io::Result<()> async fn gate(&self) -> io::Result<Self::Gate> { Ok(Self::Gate::default()) }
#[inline]
async fn hard_link<P, Q>(&self, original: P, link: Q) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
@ -62,7 +66,7 @@ impl Provider for Local {
} }
#[inline] #[inline]
async fn metadata<P>(path: P) -> io::Result<Cha> async fn metadata<P>(&self, path: P) -> io::Result<Cha>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -71,7 +75,7 @@ impl Provider for Local {
} }
#[inline] #[inline]
async fn read_dir<P>(path: P) -> io::Result<Self::ReadDir> async fn read_dir<P>(&self, path: P) -> io::Result<Self::ReadDir>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -79,7 +83,7 @@ impl Provider for Local {
} }
#[inline] #[inline]
async fn read_link<P>(path: P) -> io::Result<PathBuf> async fn read_link<P>(&self, path: P) -> io::Result<PathBuf>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -87,7 +91,7 @@ impl Provider for Local {
} }
#[inline] #[inline]
async fn remove_dir<P>(path: P) -> io::Result<()> async fn remove_dir<P>(&self, path: P) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -95,7 +99,7 @@ impl Provider for Local {
} }
#[inline] #[inline]
async fn remove_dir_all<P>(path: P) -> io::Result<()> async fn remove_dir_all<P>(&self, path: P) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -103,7 +107,7 @@ impl Provider for Local {
} }
#[inline] #[inline]
async fn remove_file<P>(path: P) -> io::Result<()> async fn remove_file<P>(&self, path: P) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -111,7 +115,7 @@ impl Provider for Local {
} }
#[inline] #[inline]
async fn rename<P, Q>(from: P, to: Q) -> io::Result<()> async fn rename<P, Q>(&self, from: P, to: Q) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
@ -120,7 +124,7 @@ impl Provider for Local {
} }
#[inline] #[inline]
async fn symlink<P, Q, F>(original: P, link: Q, _is_dir: F) -> io::Result<()> async fn symlink<P, Q, F>(&self, original: P, link: Q, _is_dir: F) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
@ -132,14 +136,14 @@ impl Provider for Local {
} }
#[cfg(windows)] #[cfg(windows)]
if _is_dir().await? { if _is_dir().await? {
Self::symlink_dir(original, link).await self.symlink_dir(original, link).await
} else { } else {
Self::symlink_file(original, link).await self.symlink_file(original, link).await
} }
} }
#[inline] #[inline]
async fn symlink_dir<P, Q>(original: P, link: Q) -> io::Result<()> async fn symlink_dir<P, Q>(&self, original: P, link: Q) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
@ -155,7 +159,7 @@ impl Provider for Local {
} }
#[inline] #[inline]
async fn symlink_file<P, Q>(original: P, link: Q) -> io::Result<()> async fn symlink_file<P, Q>(&self, original: P, link: Q) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
@ -171,7 +175,7 @@ impl Provider for Local {
} }
#[inline] #[inline]
async fn symlink_metadata<P>(path: P) -> io::Result<Cha> async fn symlink_metadata<P>(&self, path: P) -> io::Result<Cha>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -179,7 +183,7 @@ impl Provider for Local {
Ok(Cha::new(path.file_name().unwrap_or_default(), tokio::fs::symlink_metadata(path).await?)) Ok(Cha::new(path.file_name().unwrap_or_default(), tokio::fs::symlink_metadata(path).await?))
} }
async fn trash<P>(path: P) -> io::Result<()> async fn trash<P>(&self, path: P) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -205,7 +209,7 @@ impl Provider for Local {
} }
#[inline] #[inline]
async fn write<P, C>(path: P, contents: C) -> io::Result<()> async fn write<P, C>(&self, path: P, contents: C) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
C: AsRef<[u8]>, C: AsRef<[u8]>,
@ -264,7 +268,7 @@ impl Local {
} }
#[inline] #[inline]
pub async fn read<P>(path: P) -> io::Result<Vec<u8>> pub async fn read<P>(&self, path: P) -> io::Result<Vec<u8>>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -272,7 +276,7 @@ impl Local {
} }
#[inline] #[inline]
pub async fn read_to_string<P>(path: P) -> io::Result<String> pub async fn read_to_string<P>(&self, path: P) -> io::Result<String>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {

View file

@ -5,9 +5,9 @@ use crate::provider::DirReader;
pub struct ReadDir(pub(super) tokio::fs::ReadDir); pub struct ReadDir(pub(super) tokio::fs::ReadDir);
impl DirReader for ReadDir { impl DirReader for ReadDir {
type Entry<'a> = super::DirEntry; type Entry = super::DirEntry;
async fn next(&mut self) -> io::Result<Option<Self::Entry<'_>>> { async fn next(&mut self) -> io::Result<Option<Self::Entry>> {
self.0.next_entry().await.map(|entry| entry.map(super::DirEntry)) self.0.next_entry().await.map(|entry| entry.map(super::DirEntry))
} }
} }

View file

@ -1,5 +1,5 @@
yazi_macro::mod_pub!(local sftp); yazi_macro::mod_pub!(local sftp);
yazi_macro::mod_flat!(calculator dir_entry provider read_dir rw_file traits); yazi_macro::mod_flat!(calculator dir_entry gate provider providers read_dir rw_file traits);
pub fn init() { sftp::init(); } pub fn init() { sftp::init(); }

View file

@ -1,6 +1,6 @@
use std::{io, path::{Path, PathBuf}}; use std::{io, path::{Path, PathBuf}, sync::Arc};
use yazi_shared::url::{Url, UrlBuf}; use yazi_shared::{scheme::SchemeRef, url::{Url, UrlBuf}};
use crate::{cha::Cha, provider::{Provider, ReadDir, RwFile, local::{self, Local}}}; use crate::{cha::Cha, provider::{Provider, ReadDir, RwFile, local::{self, Local}}};
@ -9,7 +9,7 @@ pub fn cache<'a, U>(url: U) -> Option<PathBuf>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { Local::cache(path) } else { None } if let Some(path) = url.into().as_path() { Local.cache(path) } else { None }
} }
#[inline] #[inline]
@ -31,7 +31,7 @@ where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { if let Some(path) = url.into().as_path() {
Local::canonicalize(path).await.map(Into::into) Local.canonicalize(path).await.map(Into::into)
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -56,7 +56,7 @@ where
V: Into<Url<'a>>, V: Into<Url<'a>>,
{ {
if let (Some(from), Some(to)) = (from.into().as_path(), to.into().as_path()) { if let (Some(from), Some(to)) = (from.into().as_path(), to.into().as_path()) {
Local::copy(from, to, cha).await Local.copy(from, to, cha).await
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -68,7 +68,7 @@ where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { if let Some(path) = url.into().as_path() {
Local::create(path).await.map(Into::into) Local.create(path).await.map(Into::into)
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -80,7 +80,7 @@ where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { if let Some(path) = url.into().as_path() {
Local::create_dir(path).await Local.create_dir(path).await
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -92,7 +92,7 @@ where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { if let Some(path) = url.into().as_path() {
Local::create_dir_all(path).await Local.create_dir_all(path).await
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -105,7 +105,7 @@ where
V: Into<Url<'a>>, V: Into<Url<'a>>,
{ {
if let (Some(original), Some(link)) = (original.into().as_path(), link.into().as_path()) { if let (Some(original), Some(link)) = (original.into().as_path(), link.into().as_path()) {
Local::hard_link(original, link).await Local.hard_link(original, link).await
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -130,7 +130,7 @@ where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { if let Some(path) = url.into().as_path() {
Local::metadata(path).await Local.metadata(path).await
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -150,11 +150,17 @@ pub async fn read_dir<'a, U>(url: U) -> io::Result<ReadDir>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { let url: Url = url.into();
Local::read_dir(path).await.map(Into::into) Ok(match url.scheme {
} else { SchemeRef::Regular => ReadDir::Regular(Local.read_dir(url.loc).await?),
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) SchemeRef::Search(_) => {
} ReadDir::Search((Arc::new(url.to_owned()), Local.read_dir(url.loc).await?))
}
SchemeRef::Archive(_) => {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))?
}
SchemeRef::Sftp(_) => todo!(),
})
} }
#[inline] #[inline]
@ -163,7 +169,7 @@ where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { if let Some(path) = url.into().as_path() {
Local::read_link(path).await Local.read_link(path).await
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -175,7 +181,7 @@ where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { if let Some(path) = url.into().as_path() {
Local::remove_dir(path).await Local.remove_dir(path).await
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -187,7 +193,7 @@ where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { if let Some(path) = url.into().as_path() {
Local::remove_dir_all(path).await Local.remove_dir_all(path).await
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -199,7 +205,7 @@ where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { if let Some(path) = url.into().as_path() {
Local::remove_file(path).await Local.remove_file(path).await
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -212,7 +218,7 @@ where
V: Into<Url<'a>>, V: Into<Url<'a>>,
{ {
if let (Some(from), Some(to)) = (from.into().as_path(), to.into().as_path()) { if let (Some(from), Some(to)) = (from.into().as_path(), to.into().as_path()) {
Local::rename(from, to).await Local.rename(from, to).await
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -225,7 +231,7 @@ where
F: AsyncFnOnce() -> io::Result<bool>, F: AsyncFnOnce() -> io::Result<bool>,
{ {
if let Some(link) = link.into().as_path() { if let Some(link) = link.into().as_path() {
Local::symlink(original, link, is_dir).await Local.symlink(original, link, is_dir).await
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -237,7 +243,7 @@ where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(link) = link.into().as_path() { if let Some(link) = link.into().as_path() {
Local::symlink_dir(original, link).await Local.symlink_dir(original, link).await
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -249,7 +255,7 @@ where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(link) = link.into().as_path() { if let Some(link) = link.into().as_path() {
Local::symlink_file(original, link).await Local.symlink_file(original, link).await
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -261,7 +267,7 @@ where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { if let Some(path) = url.into().as_path() {
Local::symlink_metadata(path).await Local.symlink_metadata(path).await
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -273,7 +279,7 @@ where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { if let Some(path) = url.into().as_path() {
Local::trash(path).await Local.trash(path).await
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }
@ -286,7 +292,7 @@ where
C: AsRef<[u8]>, C: AsRef<[u8]>,
{ {
if let Some(path) = url.into().as_path() { if let Some(path) = url.into().as_path() {
Local::write(path, contents).await Local.write(path, contents).await
} else { } else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
} }

View file

@ -0,0 +1,266 @@
use std::{io, path::{Path, PathBuf}, sync::Arc};
use yazi_shared::{scheme::SchemeRef, url::Url};
use yazi_vfs::config::{ProviderSftp, Vfs};
use super::local::Local;
use crate::{cha::Cha, provider::Provider};
pub(super) struct Providers<'a>(Inner<'a>);
enum Inner<'a> {
Regular,
Search(Url<'a>),
Sftp((super::sftp::Sftp, Url<'a>)),
}
impl<'a> Providers<'a> {
pub(super) async fn new(url: Url<'a>) -> io::Result<Self> {
Ok(match url.scheme {
SchemeRef::Regular => Self(Inner::Regular),
SchemeRef::Search(_) => Self(Inner::Search(url)),
SchemeRef::Archive(_) => {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))?
}
SchemeRef::Sftp(name) => {
Self(Inner::Sftp((Vfs::provider::<&ProviderSftp>(name).await?.into(), url)))
}
})
}
}
impl Provider for Providers<'_> {
type File = super::RwFile;
type Gate = super::Gate;
type ReadDir = super::ReadDir;
fn cache<P>(&self, path: P) -> Option<PathBuf>
where
P: AsRef<Path>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.cache(path),
Inner::Sftp((p, _)) => p.cache(path),
}
}
async fn canonicalize<P>(&self, path: P) -> io::Result<PathBuf>
where
P: AsRef<Path>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.canonicalize(path).await,
Inner::Sftp((p, _)) => p.canonicalize(path).await,
}
}
async fn copy<P, Q>(&self, from: P, to: Q, cha: Cha) -> io::Result<u64>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.copy(from, to, cha).await,
Inner::Sftp((p, _)) => p.copy(from, to, cha).await,
}
}
async fn create<P>(&self, path: P) -> io::Result<Self::File>
where
P: AsRef<Path>,
{
Ok(match self.0 {
Inner::Regular | Inner::Search(_) => Local.create(path).await?.into(),
Inner::Sftp((p, _)) => p.create(path).await?.into(),
})
}
async fn create_dir<P>(&self, path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.create_dir(path).await,
Inner::Sftp((p, _)) => p.create_dir(path).await,
}
}
async fn create_dir_all<P>(&self, path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.create_dir_all(path).await,
Inner::Sftp((p, _)) => p.create_dir_all(path).await,
}
}
async fn gate(&self) -> io::Result<Self::Gate> {
Ok(match self.0 {
Inner::Regular | Inner::Search(_) => Local.gate().await?.into(),
Inner::Sftp((p, _)) => p.gate().await?.into(),
})
}
async fn hard_link<P, Q>(&self, original: P, link: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.hard_link(original, link).await,
Inner::Sftp((p, _)) => p.hard_link(original, link).await,
}
}
async fn metadata<P>(&self, path: P) -> io::Result<Cha>
where
P: AsRef<Path>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.metadata(path).await,
Inner::Sftp((p, _)) => p.metadata(path).await,
}
}
async fn open<P>(&self, path: P) -> io::Result<Self::File>
where
P: AsRef<Path>,
{
Ok(match self.0 {
Inner::Regular | Inner::Search(_) => Local.open(path).await?.into(),
Inner::Sftp((p, _)) => p.open(path).await?.into(),
})
}
async fn read_dir<P>(&self, path: P) -> io::Result<Self::ReadDir>
where
P: AsRef<Path>,
{
Ok(match self.0 {
Inner::Regular => Self::ReadDir::Regular(Local.read_dir(path).await?),
Inner::Search(dir) => {
Self::ReadDir::Search((Arc::new(dir.to_owned()), Local.read_dir(path).await?))
}
Inner::Sftp((p, dir)) => {
Self::ReadDir::Sftp((Arc::new(dir.to_owned()), p.read_dir(path).await?))
}
})
}
async fn read_link<P>(&self, path: P) -> io::Result<PathBuf>
where
P: AsRef<Path>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.read_link(path).await,
Inner::Sftp((p, _)) => p.read_link(path).await,
}
}
async fn remove_dir<P>(&self, path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.remove_dir(path).await,
Inner::Sftp((p, _)) => p.remove_dir(path).await,
}
}
async fn remove_dir_all<P>(&self, path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.remove_dir_all(path).await,
Inner::Sftp((p, _)) => p.remove_dir_all(path).await,
}
}
async fn remove_file<P>(&self, path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.remove_file(path).await,
Inner::Sftp((p, _)) => p.remove_file(path).await,
}
}
async fn rename<P, Q>(&self, from: P, to: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.rename(from, to).await,
Inner::Sftp((p, _)) => p.rename(from, to).await,
}
}
async fn symlink<P, Q, F>(&self, original: P, link: Q, is_dir: F) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
F: AsyncFnOnce() -> io::Result<bool>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.symlink(original, link, is_dir).await,
Inner::Sftp((p, _)) => p.symlink(original, link, is_dir).await,
}
}
async fn symlink_dir<P, Q>(&self, original: P, link: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.symlink_dir(original, link).await,
Inner::Sftp((p, _)) => p.symlink_dir(original, link).await,
}
}
async fn symlink_file<P, Q>(&self, original: P, link: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.symlink_file(original, link).await,
Inner::Sftp((p, _)) => p.symlink_file(original, link).await,
}
}
async fn symlink_metadata<P>(&self, path: P) -> io::Result<Cha>
where
P: AsRef<Path>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.symlink_metadata(path).await,
Inner::Sftp((p, _)) => p.symlink_metadata(path).await,
}
}
async fn trash<P>(&self, path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.trash(path).await,
Inner::Sftp((p, _)) => p.trash(path).await,
}
}
async fn write<P, C>(&self, path: P, contents: C) -> io::Result<()>
where
P: AsRef<Path>,
C: AsRef<[u8]>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.write(path, contents).await,
Inner::Sftp((p, _)) => p.write(path, contents).await,
}
}
}

View file

@ -1,20 +1,27 @@
use std::io; use std::{io, sync::Arc};
use yazi_shared::url::UrlBuf;
use super::DirEntry;
use crate::provider::DirReader; use crate::provider::DirReader;
pub enum ReadDir { pub enum ReadDir {
Local(super::local::ReadDir), Regular(super::local::ReadDir),
Search((Arc<UrlBuf>, super::local::ReadDir)),
Sftp((Arc<UrlBuf>, super::sftp::ReadDir)),
} }
impl From<super::local::ReadDir> for ReadDir { impl DirReader for ReadDir {
fn from(value: super::local::ReadDir) -> Self { Self::Local(value) } type Entry = super::DirEntry;
}
impl ReadDir { async fn next(&mut self) -> io::Result<Option<Self::Entry>> {
pub async fn next_entry(&mut self) -> io::Result<Option<DirEntry>> { Ok(match self {
match self { Self::Regular(reader) => reader.next().await?.map(Self::Entry::Regular),
Self::Local(local) => local.next().await.map(|entry| entry.map(Into::into)), Self::Search((dir, reader)) => {
} reader.next().await?.map(|ent| Self::Entry::Search((dir.clone(), ent)))
}
Self::Sftp((dir, reader)) => {
reader.next().await?.map(|ent| Self::Entry::Sftp((dir.clone(), ent)))
}
})
} }
} }

View file

@ -4,7 +4,7 @@ use tokio::io::{AsyncRead, AsyncWrite};
pub enum RwFile { pub enum RwFile {
Tokio(tokio::fs::File), Tokio(tokio::fs::File),
SFTP(Box<yazi_sftp::fs::File>), Sftp(Box<yazi_sftp::fs::File>),
} }
impl From<tokio::fs::File> for RwFile { impl From<tokio::fs::File> for RwFile {
@ -12,7 +12,7 @@ impl From<tokio::fs::File> for RwFile {
} }
impl From<yazi_sftp::fs::File> for RwFile { impl From<yazi_sftp::fs::File> for RwFile {
fn from(f: yazi_sftp::fs::File) -> Self { Self::SFTP(Box::new(f)) } fn from(f: yazi_sftp::fs::File) -> Self { Self::Sftp(Box::new(f)) }
} }
impl AsyncRead for RwFile { impl AsyncRead for RwFile {
@ -24,7 +24,7 @@ impl AsyncRead for RwFile {
) -> std::task::Poll<std::io::Result<()>> { ) -> std::task::Poll<std::io::Result<()>> {
match &mut *self { match &mut *self {
Self::Tokio(f) => Pin::new(f).poll_read(cx, buf), Self::Tokio(f) => Pin::new(f).poll_read(cx, buf),
Self::SFTP(f) => Pin::new(f).poll_read(cx, buf), Self::Sftp(f) => Pin::new(f).poll_read(cx, buf),
} }
} }
} }
@ -38,7 +38,7 @@ impl AsyncWrite for RwFile {
) -> std::task::Poll<Result<usize, std::io::Error>> { ) -> std::task::Poll<Result<usize, std::io::Error>> {
match &mut *self { match &mut *self {
Self::Tokio(f) => Pin::new(f).poll_write(cx, buf), Self::Tokio(f) => Pin::new(f).poll_write(cx, buf),
Self::SFTP(f) => Pin::new(f).poll_write(cx, buf), Self::Sftp(f) => Pin::new(f).poll_write(cx, buf),
} }
} }
@ -49,7 +49,7 @@ impl AsyncWrite for RwFile {
) -> std::task::Poll<Result<(), std::io::Error>> { ) -> std::task::Poll<Result<(), std::io::Error>> {
match &mut *self { match &mut *self {
Self::Tokio(f) => Pin::new(f).poll_flush(cx), Self::Tokio(f) => Pin::new(f).poll_flush(cx),
Self::SFTP(f) => Pin::new(f).poll_flush(cx), Self::Sftp(f) => Pin::new(f).poll_flush(cx),
} }
} }
@ -60,7 +60,7 @@ impl AsyncWrite for RwFile {
) -> std::task::Poll<Result<(), std::io::Error>> { ) -> std::task::Poll<Result<(), std::io::Error>> {
match &mut *self { match &mut *self {
Self::Tokio(f) => Pin::new(f).poll_shutdown(cx), Self::Tokio(f) => Pin::new(f).poll_shutdown(cx),
Self::SFTP(f) => Pin::new(f).poll_shutdown(cx), Self::Sftp(f) => Pin::new(f).poll_shutdown(cx),
} }
} }
@ -72,7 +72,7 @@ impl AsyncWrite for RwFile {
) -> std::task::Poll<Result<usize, std::io::Error>> { ) -> std::task::Poll<Result<usize, std::io::Error>> {
match &mut *self { match &mut *self {
Self::Tokio(f) => Pin::new(f).poll_write_vectored(cx, bufs), Self::Tokio(f) => Pin::new(f).poll_write_vectored(cx, bufs),
Self::SFTP(f) => Pin::new(f).poll_write_vectored(cx, bufs), Self::Sftp(f) => Pin::new(f).poll_write_vectored(cx, bufs),
} }
} }
@ -80,7 +80,7 @@ impl AsyncWrite for RwFile {
fn is_write_vectored(&self) -> bool { fn is_write_vectored(&self) -> bool {
match self { match self {
Self::Tokio(f) => f.is_write_vectored(), Self::Tokio(f) => f.is_write_vectored(),
Self::SFTP(f) => f.is_write_vectored(), Self::Sftp(f) => f.is_write_vectored(),
} }
} }
} }

View file

@ -1,11 +1,14 @@
use std::{io, path::Path}; use std::{io, path::Path};
use yazi_sftp::fs::{Attrs, Flags}; use yazi_sftp::fs::{Attrs, Flags};
use yazi_shared::scheme::SchemeRef;
use yazi_vfs::config::{ProviderSftp, Vfs};
use crate::provider::FileBuilder; use crate::provider::FileBuilder;
#[derive(Default)]
pub struct Gate { pub struct Gate {
sftp: super::Sftp,
append: bool, append: bool,
create: bool, create: bool,
create_new: bool, create_new: bool,
@ -32,6 +35,24 @@ impl FileBuilder for Gate {
self self
} }
async fn new(scheme: SchemeRef<'_>) -> io::Result<Self> {
let sftp: super::Sftp = match scheme {
SchemeRef::Sftp(name) => Vfs::provider::<&ProviderSftp>(name).await?.into(),
_ => Err(io::Error::new(io::ErrorKind::InvalidInput, "Not an SFTP URL"))?,
};
Ok(Self {
sftp,
append: false,
create: false,
create_new: false,
read: false,
truncate: false,
write: false,
})
}
async fn open<P>(&self, path: P) -> io::Result<Self::File> async fn open<P>(&self, path: P) -> io::Result<Self::File>
where where
P: AsRef<Path>, P: AsRef<Path>,
@ -55,7 +76,8 @@ impl FileBuilder for Gate {
if self.write { if self.write {
flags |= Flags::WRITE; flags |= Flags::WRITE;
} }
Ok(super::Sftp::op().await?.open(&path, flags, Attrs::default()).await?)
Ok(self.sftp.op().await?.open(&path, flags, Attrs::default()).await?)
} }
fn read(&mut self, read: bool) -> &mut Self { fn read(&mut self, read: bool) -> &mut Self {

View file

@ -2,10 +2,10 @@ use std::{ffi::OsStr, io, time::{Duration, UNIX_EPOCH}};
use crate::cha::{Cha, ChaKind, ChaMode}; use crate::cha::{Cha, ChaKind, ChaMode};
impl TryFrom<yazi_sftp::fs::DirEntry<'_>> for Cha { impl TryFrom<yazi_sftp::fs::DirEntry> for Cha {
type Error = io::Error; type Error = io::Error;
fn try_from(ent: yazi_sftp::fs::DirEntry<'_>) -> Result<Self, Self::Error> { fn try_from(ent: yazi_sftp::fs::DirEntry) -> Result<Self, Self::Error> {
let mut cha = Self::try_from((ent.name().as_ref(), ent.attrs()))?; let mut cha = Self::try_from((ent.name().as_ref(), ent.attrs()))?;
cha.nlink = ent.nlink().unwrap_or_default(); cha.nlink = ent.nlink().unwrap_or_default();
Ok(cha) Ok(cha)

View file

@ -1,6 +1,12 @@
yazi_macro::mod_flat!(gate metadata read_dir sftp); yazi_macro::mod_flat!(gate metadata read_dir sftp);
pub(super) static CONN: yazi_shared::RoCell<deadpool::managed::Pool<Sftp>> = pub(super) static CONN: yazi_shared::RoCell<
yazi_shared::RoCell::new(); parking_lot::Mutex<
hashbrown::HashMap<
&'static yazi_vfs::config::ProviderSftp,
&'static deadpool::managed::Pool<Sftp>,
>,
>,
> = yazi_shared::RoCell::new();
pub(super) fn init() { CONN.init(deadpool::managed::Pool::builder(Sftp).build().unwrap()); } pub(super) fn init() { CONN.init(Default::default()); }

View file

@ -5,17 +5,17 @@ use crate::{cha::{Cha, ChaMode, ChaType}, provider::{DirReader, FileHolder}};
pub struct ReadDir(pub(super) yazi_sftp::fs::ReadDir); pub struct ReadDir(pub(super) yazi_sftp::fs::ReadDir);
impl DirReader for ReadDir { impl DirReader for ReadDir {
type Entry<'a> = DirEntry<'a>; type Entry = DirEntry;
async fn next(&mut self) -> io::Result<Option<Self::Entry<'_>>> { async fn next(&mut self) -> io::Result<Option<Self::Entry>> {
Ok(self.0.next().await?.map(DirEntry)) Ok(self.0.next().await?.map(DirEntry))
} }
} }
// --- Entry // --- Entry
pub struct DirEntry<'a>(yazi_sftp::fs::DirEntry<'a>); pub struct DirEntry(yazi_sftp::fs::DirEntry);
impl FileHolder for DirEntry<'_> { impl FileHolder for DirEntry {
fn path(&self) -> PathBuf { self.0.path() } fn path(&self) -> PathBuf { self.0.path() }
fn name(&self) -> Cow<'_, OsStr> { self.0.name() } fn name(&self) -> Cow<'_, OsStr> { self.0.name() }

View file

@ -1,123 +1,137 @@
use std::{io, path::{Path, PathBuf}}; use std::{io, path::{Path, PathBuf}, sync::Arc};
use yazi_sftp::fs::{Attrs, Flags}; use yazi_sftp::fs::{Attrs, Flags};
use yazi_shared::scheme::SchemeRef;
use yazi_vfs::config::ProviderSftp;
use crate::{cha::Cha, provider::Provider}; use crate::{cha::Cha, provider::{FileBuilder, Provider}};
pub struct Sftp; #[derive(Clone, Copy)]
pub struct Sftp {
name: &'static str,
config: &'static ProviderSftp,
}
impl From<(&'static str, &'static ProviderSftp)> for Sftp {
fn from((name, config): (&'static str, &'static ProviderSftp)) -> Self { Self { name, config } }
}
impl Provider for Sftp { impl Provider for Sftp {
type File = yazi_sftp::fs::File; type File = yazi_sftp::fs::File;
type Gate = super::Gate; type Gate = super::Gate;
type ReadDir = super::ReadDir; type ReadDir = super::ReadDir;
fn cache<P>(_: P) -> Option<PathBuf> fn cache<P>(&self, _: P) -> Option<PathBuf>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
todo!() todo!()
} }
async fn canonicalize<P>(path: P) -> io::Result<PathBuf> async fn canonicalize<P>(&self, path: P) -> io::Result<PathBuf>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
Ok(Self::op().await?.realpath(&path).await?) Ok(self.op().await?.realpath(&path).await?)
} }
async fn copy<P, Q>(from: P, to: Q, cha: Cha) -> io::Result<u64> async fn copy<P, Q>(&self, from: P, to: Q, cha: Cha) -> io::Result<u64>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
{ {
let attrs = Attrs::from(cha); let attrs = Attrs::from(cha);
let op = Self::op().await?; let op = self.op().await?;
let mut from = op.open(&from, Flags::READ, Attrs::default()).await?; let mut from = op.open(&from, Flags::READ, Attrs::default()).await?;
let mut to = op.open(&to, Flags::WRITE | Flags::CREATE | Flags::TRUNCATE, attrs).await?; let mut to = op.open(&to, Flags::WRITE | Flags::CREATE | Flags::TRUNCATE, attrs).await?;
tokio::io::copy(&mut from, &mut to).await tokio::io::copy(&mut from, &mut to).await
} }
async fn create_dir<P>(path: P) -> io::Result<()> async fn create_dir<P>(&self, path: P) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
Ok(Self::op().await?.mkdir(&path, Attrs::default()).await?) Ok(self.op().await?.mkdir(&path, Attrs::default()).await?)
} }
async fn hard_link<P, Q>(original: P, link: Q) -> io::Result<()> async fn gate(&self) -> io::Result<Self::Gate> {
super::Gate::new(SchemeRef::Sftp(self.name)).await
}
async fn hard_link<P, Q>(&self, original: P, link: Q) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
{ {
Ok(Self::op().await?.hardlink(&original, &link).await?) Ok(self.op().await?.hardlink(&original, &link).await?)
} }
async fn metadata<P>(path: P) -> io::Result<Cha> async fn metadata<P>(&self, path: P) -> io::Result<Cha>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let path = path.as_ref(); let path = path.as_ref();
let attrs = Self::op().await?.stat(path).await?; let attrs = self.op().await?.stat(path).await?;
(path.file_name().unwrap_or_default(), &attrs).try_into() (path.file_name().unwrap_or_default(), &attrs).try_into()
} }
async fn read_dir<P>(path: P) -> io::Result<Self::ReadDir> async fn read_dir<P>(&self, path: P) -> io::Result<Self::ReadDir>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
Ok(super::ReadDir(Self::op().await?.read_dir(&path).await?)) Ok(super::ReadDir(self.op().await?.read_dir(&path).await?))
} }
async fn read_link<P>(path: P) -> io::Result<PathBuf> async fn read_link<P>(&self, path: P) -> io::Result<PathBuf>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
Ok(Self::op().await?.readlink(&path).await?) Ok(self.op().await?.readlink(&path).await?)
} }
async fn remove_dir<P>(path: P) -> io::Result<()> async fn remove_dir<P>(&self, path: P) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
Ok(Self::op().await?.rmdir(&path).await?) Ok(self.op().await?.rmdir(&path).await?)
} }
async fn remove_file<P>(path: P) -> io::Result<()> async fn remove_file<P>(&self, path: P) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
Ok(Self::op().await?.remove(&path).await?) Ok(self.op().await?.remove(&path).await?)
} }
async fn rename<P, Q>(from: P, to: Q) -> io::Result<()> async fn rename<P, Q>(&self, from: P, to: Q) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
{ {
Ok(Self::op().await?.rename(&from, &to).await?) Ok(self.op().await?.rename(&from, &to).await?)
} }
async fn symlink<P, Q, F>(original: P, link: Q, _is_dir: F) -> io::Result<()> async fn symlink<P, Q, F>(&self, original: P, link: Q, _is_dir: F) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
F: AsyncFnOnce() -> io::Result<bool>, F: AsyncFnOnce() -> io::Result<bool>,
{ {
Ok(Self::op().await?.symlink(&original, &link).await?) Ok(self.op().await?.symlink(&original, &link).await?)
} }
async fn symlink_metadata<P>(path: P) -> io::Result<Cha> async fn symlink_metadata<P>(&self, path: P) -> io::Result<Cha>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let path = path.as_ref(); let path = path.as_ref();
let attrs = Self::op().await?.lstat(path).await?; let attrs = self.op().await?.lstat(path).await?;
(path.file_name().unwrap_or_default(), &attrs).try_into() (path.file_name().unwrap_or_default(), &attrs).try_into()
} }
async fn trash<P>(_path: P) -> io::Result<()> async fn trash<P>(&self, _path: P) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -126,10 +140,14 @@ impl Provider for Sftp {
} }
impl Sftp { impl Sftp {
pub(super) async fn op() -> io::Result<deadpool::managed::Object<Sftp>> { pub(super) async fn op(self) -> io::Result<deadpool::managed::Object<Sftp>> {
use deadpool::managed::PoolError; use deadpool::managed::PoolError;
super::CONN.get().await.map_err(|e| match e { let pool = *super::CONN.lock().entry(self.config).or_insert_with(|| {
Box::leak(Box::new(deadpool::managed::Pool::builder(self).build().unwrap()))
});
pool.get().await.map_err(|e| match e {
PoolError::Timeout(_) => io::Error::new(io::ErrorKind::TimedOut, e.to_string()), PoolError::Timeout(_) => io::Error::new(io::ErrorKind::TimedOut, e.to_string()),
PoolError::Backend(e) => e, PoolError::Backend(e) => e,
PoolError::Closed | PoolError::NoRuntimeSpecified | PoolError::PostCreateHook(_) => { PoolError::Closed | PoolError::NoRuntimeSpecified | PoolError::PostCreateHook(_) => {
@ -139,19 +157,60 @@ impl Sftp {
} }
} }
impl russh::client::Handler for Sftp {
type Error = russh::Error;
async fn check_server_key(
&mut self,
_server_public_key: &russh::keys::PublicKey,
) -> Result<bool, Self::Error> {
Ok(true)
}
}
impl deadpool::managed::Manager for Sftp { impl deadpool::managed::Manager for Sftp {
type Error = io::Error; type Error = io::Error;
type Type = yazi_sftp::Operator; type Type = yazi_sftp::Operator;
async fn create(&self) -> Result<Self::Type, Self::Error> { todo!() } // FIXME: remove the hardcoded test values
async fn create(&self) -> Result<Self::Type, Self::Error> {
todo!()
// async fn inner(sftp: Sftp) -> anyhow::Result<yazi_sftp::Operator> {
// let config = Arc::new(russh::client::Config::default());
// let mut session = russh::client::connect(config, ("127.0.0.1", 22),
// sftp).await?;
// let mut agent = russh::keys::agent::client::AgentClient::connect_uds(
// "/Users/ika/Library/Group
// Containers/2BUA8C4S2C.com.1password/t/agent.sock", )
// .await?;
// let mut keys = agent.request_identities().await?;
// if !session
// .authenticate_publickey_with("root", keys.remove(0), None, &mut agent)
// .await?
// .success()
// {
// panic!("auth failed");
// }
// let channel = session.channel_open_session().await?;
// channel.request_subsystem(true, "sftp").await?;
// let mut op = yazi_sftp::Operator::make(channel.into_stream());
// op.init().await?;
// Ok(op)
// }
// inner(*self).await.map_err(|e| io::Error::other(e.to_string()))
}
async fn recycle( async fn recycle(
&self, &self,
obj: &mut Self::Type, obj: &mut Self::Type,
metrics: &deadpool::managed::Metrics, metrics: &deadpool::managed::Metrics,
) -> deadpool::managed::RecycleResult<Self::Error> { ) -> deadpool::managed::RecycleResult<Self::Error> {
todo!() // FIXME
Ok(())
} }
fn detach(&self, _obj: &mut Self::Type) { todo!() }
} }

View file

@ -2,6 +2,7 @@ use std::{borrow::Cow, ffi::OsStr, io, path::{Path, PathBuf}};
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
use yazi_macro::ok_or_not_found; use yazi_macro::ok_or_not_found;
use yazi_shared::scheme::SchemeRef;
use crate::cha::{Cha, ChaType}; use crate::cha::{Cha, ChaType};
@ -10,31 +11,31 @@ pub trait Provider {
type Gate: FileBuilder<File = Self::File>; type Gate: FileBuilder<File = Self::File>;
type ReadDir: DirReader; type ReadDir: DirReader;
fn cache<P>(_: P) -> Option<PathBuf> fn cache<P>(&self, path: P) -> Option<PathBuf>
where where
P: AsRef<Path>; P: AsRef<Path>;
fn canonicalize<P>(path: P) -> impl Future<Output = io::Result<PathBuf>> fn canonicalize<P>(&self, path: P) -> impl Future<Output = io::Result<PathBuf>>
where where
P: AsRef<Path>; P: AsRef<Path>;
fn copy<P, Q>(from: P, to: Q, cha: Cha) -> impl Future<Output = io::Result<u64>> fn copy<P, Q>(&self, from: P, to: Q, cha: Cha) -> impl Future<Output = io::Result<u64>>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>; Q: AsRef<Path>;
fn create<P>(path: P) -> impl Future<Output = io::Result<Self::File>> fn create<P>(&self, path: P) -> impl Future<Output = io::Result<Self::File>>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
async move { Self::Gate::default().write(true).create(true).truncate(true).open(path).await } async move { self.gate().await?.write(true).create(true).truncate(true).open(path).await }
} }
fn create_dir<P>(path: P) -> impl Future<Output = io::Result<()>> fn create_dir<P>(&self, path: P) -> impl Future<Output = io::Result<()>>
where where
P: AsRef<Path>; P: AsRef<Path>;
fn create_dir_all<P>(path: P) -> impl Future<Output = io::Result<()>> fn create_dir_all<P>(&self, path: P) -> impl Future<Output = io::Result<()>>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -44,147 +45,154 @@ pub trait Provider {
return Ok(()); return Ok(());
} }
match Self::create_dir(path).await { match self.create_dir(path).await {
Ok(()) => return Ok(()), Ok(()) => return Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => {} Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(_) if Self::metadata(path).await.is_ok_and(|m| m.is_dir()) => return Ok(()), Err(_) if self.metadata(path).await.is_ok_and(|m| m.is_dir()) => return Ok(()),
Err(e) => return Err(e), Err(e) => return Err(e),
} }
match path.parent() { match path.parent() {
Some(p) => Self::create_dir_all(p).await?, Some(p) => self.create_dir_all(p).await?,
None => return Err(io::Error::other("failed to create whole tree")), None => return Err(io::Error::other("failed to create whole tree")),
} }
match Self::create_dir(path).await { match self.create_dir(path).await {
Ok(()) => Ok(()), Ok(()) => Ok(()),
Err(_) if Self::metadata(path).await.is_ok_and(|m| m.is_dir()) => Ok(()), Err(_) if self.metadata(path).await.is_ok_and(|m| m.is_dir()) => Ok(()),
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
} }
fn hard_link<P, Q>(original: P, link: Q) -> impl Future<Output = io::Result<()>> fn gate(&self) -> impl Future<Output = io::Result<Self::Gate>>;
fn hard_link<P, Q>(&self, original: P, link: Q) -> impl Future<Output = io::Result<()>>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>; Q: AsRef<Path>;
fn metadata<P>(path: P) -> impl Future<Output = io::Result<Cha>> fn metadata<P>(&self, path: P) -> impl Future<Output = io::Result<Cha>>
where where
P: AsRef<Path>; P: AsRef<Path>;
fn open<P>(path: P) -> impl Future<Output = io::Result<Self::File>> fn open<P>(&self, path: P) -> impl Future<Output = io::Result<Self::File>>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
async move { Self::Gate::default().read(true).open(path).await } async move { self.gate().await?.read(true).open(path).await }
} }
fn read_dir<P>(path: P) -> impl Future<Output = io::Result<Self::ReadDir>> fn read_dir<P>(&self, path: P) -> impl Future<Output = io::Result<Self::ReadDir>>
where where
P: AsRef<Path>; P: AsRef<Path>;
fn read_link<P>(path: P) -> impl Future<Output = io::Result<PathBuf>> fn read_link<P>(&self, path: P) -> impl Future<Output = io::Result<PathBuf>>
where where
P: AsRef<Path>; P: AsRef<Path>;
fn remove_dir<P>(path: P) -> impl Future<Output = io::Result<()>> fn remove_dir<P>(&self, path: P) -> impl Future<Output = io::Result<()>>
where where
P: AsRef<Path>; P: AsRef<Path>;
fn remove_dir_all<P>(path: P) -> impl Future<Output = io::Result<()>> fn remove_dir_all<P>(&self, path: P) -> impl Future<Output = io::Result<()>>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
async fn remove_dir_all_impl<S>(path: &Path) -> io::Result<()> async fn remove_dir_all_impl<P>(me: &P, path: &Path) -> io::Result<()>
where where
S: Provider + ?Sized, P: Provider + ?Sized,
{ {
let mut it = ok_or_not_found!(S::read_dir(path).await, return Ok(())); let mut it = ok_or_not_found!(me.read_dir(path).await, return Ok(()));
while let Some(child) = it.next().await? { while let Some(child) = it.next().await? {
let ft = ok_or_not_found!(child.file_type().await, continue); let ft = ok_or_not_found!(child.file_type().await, continue);
let result = if ft.is_dir() { let result = if ft.is_dir() {
remove_dir_all_impl::<S>(&child.path()).await remove_dir_all_impl(me, &child.path()).await
} else { } else {
S::remove_file(&child.path()).await me.remove_file(&child.path()).await
}; };
() = ok_or_not_found!(result); () = ok_or_not_found!(result);
} }
Ok(ok_or_not_found!(S::remove_dir(path).await)) Ok(ok_or_not_found!(me.remove_dir(path).await))
} }
async move { async move {
let path = path.as_ref(); let path = path.as_ref();
let cha = ok_or_not_found!(Self::symlink_metadata(path).await, return Ok(())); let cha = ok_or_not_found!(self.symlink_metadata(path).await, return Ok(()));
if cha.is_link() { if cha.is_link() {
Self::remove_file(path).await self.remove_file(path).await
} else { } else {
remove_dir_all_impl::<Self>(path).await remove_dir_all_impl(self, path).await
} }
} }
} }
fn remove_file<P>(path: P) -> impl Future<Output = io::Result<()>> fn remove_file<P>(&self, path: P) -> impl Future<Output = io::Result<()>>
where where
P: AsRef<Path>; P: AsRef<Path>;
fn rename<P, Q>(from: P, to: Q) -> impl Future<Output = io::Result<()>> fn rename<P, Q>(&self, from: P, to: Q) -> impl Future<Output = io::Result<()>>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>; Q: AsRef<Path>;
fn symlink<P, Q, F>(original: P, link: Q, _is_dir: F) -> impl Future<Output = io::Result<()>> fn symlink<P, Q, F>(
&self,
original: P,
link: Q,
_is_dir: F,
) -> impl Future<Output = io::Result<()>>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
F: AsyncFnOnce() -> io::Result<bool>; F: AsyncFnOnce() -> io::Result<bool>;
fn symlink_dir<P, Q>(original: P, link: Q) -> impl Future<Output = io::Result<()>> fn symlink_dir<P, Q>(&self, original: P, link: Q) -> impl Future<Output = io::Result<()>>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
{ {
Self::symlink(original, link, async || Ok(true)) self.symlink(original, link, async || Ok(true))
} }
fn symlink_file<P, Q>(original: P, link: Q) -> impl Future<Output = io::Result<()>> fn symlink_file<P, Q>(&self, original: P, link: Q) -> impl Future<Output = io::Result<()>>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
{ {
Self::symlink(original, link, async || Ok(false)) self.symlink(original, link, async || Ok(false))
} }
fn symlink_metadata<P>(path: P) -> impl Future<Output = io::Result<Cha>> fn symlink_metadata<P>(&self, path: P) -> impl Future<Output = io::Result<Cha>>
where where
P: AsRef<Path>; P: AsRef<Path>;
fn trash<P>(path: P) -> impl Future<Output = io::Result<()>> fn trash<P>(&self, path: P) -> impl Future<Output = io::Result<()>>
where where
P: AsRef<Path>; P: AsRef<Path>;
fn write<P, C>(path: P, contents: C) -> impl Future<Output = io::Result<()>> fn write<P, C>(&self, path: P, contents: C) -> impl Future<Output = io::Result<()>>
where where
P: AsRef<Path>, P: AsRef<Path>,
C: AsRef<[u8]>, C: AsRef<[u8]>,
{ {
async move { Self::create(path).await?.write_all(contents.as_ref()).await } async move { self.create(path).await?.write_all(contents.as_ref()).await }
} }
} }
// --- DirReader // --- DirReader
pub trait DirReader { pub trait DirReader {
type Entry<'a>: FileHolder type Entry: FileHolder;
where
Self: 'a;
fn next(&mut self) -> impl Future<Output = io::Result<Option<Self::Entry<'_>>>>; fn next(&mut self) -> impl Future<Output = io::Result<Option<Self::Entry>>>;
} }
// --- FileHolder // --- FileHolder
pub trait FileHolder { pub trait FileHolder {
#[must_use]
fn path(&self) -> PathBuf; fn path(&self) -> PathBuf;
#[must_use]
fn name(&self) -> Cow<'_, OsStr>; fn name(&self) -> Cow<'_, OsStr>;
fn metadata(&self) -> impl Future<Output = io::Result<Cha>>; fn metadata(&self) -> impl Future<Output = io::Result<Cha>>;
@ -193,7 +201,7 @@ pub trait FileHolder {
} }
// --- FileOpener // --- FileOpener
pub trait FileBuilder: Default { pub trait FileBuilder {
type File: AsyncRead + AsyncWrite + Unpin; type File: AsyncRead + AsyncWrite + Unpin;
fn append(&mut self, append: bool) -> &mut Self; fn append(&mut self, append: bool) -> &mut Self;
@ -202,6 +210,10 @@ pub trait FileBuilder: Default {
fn create_new(&mut self, create_new: bool) -> &mut Self; fn create_new(&mut self, create_new: bool) -> &mut Self;
fn new(scheme: SchemeRef) -> impl Future<Output = io::Result<Self>>
where
Self: Sized;
fn open<P>(&self, path: P) -> impl Future<Output = io::Result<Self::File>> fn open<P>(&self, path: P) -> impl Future<Output = io::Result<Self::File>>
where where
P: AsRef<Path>; P: AsRef<Path>;

View file

@ -8,6 +8,6 @@ macro_rules! ok_or_not_found {
} }
}; };
($result:expr) => { ($result:expr) => {
ok_or_not_found!($result, ()) ok_or_not_found!($result, Default::default())
}; };
} }

View file

@ -2,7 +2,7 @@ use mlua::{FromLua, IntoLua, Lua, LuaSerdeExt, Value};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use yazi_shared::event::{CmdCow, Data, EventQuit}; use yazi_shared::event::{CmdCow, Data, EventQuit};
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Deserialize, Serialize)]
pub struct QuitOpt { pub struct QuitOpt {
pub code: i32, pub code: i32,
pub no_cwd_file: bool, pub no_cwd_file: bool,

View file

@ -12,7 +12,7 @@ type Iter = yazi_binding::Iter<
yazi_binding::Url, yazi_binding::Url,
>; >;
#[derive(Clone, Debug, Default, Serialize, Deserialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct UpdateYankedOpt<'a> { pub struct UpdateYankedOpt<'a> {
pub cut: bool, pub cut: bool,
pub urls: Cow<'a, HashSet<UrlBufCov>>, pub urls: Cow<'a, HashSet<UrlBufCov>>,

View file

@ -39,7 +39,7 @@ impl Highlighter {
pub fn abort() { INCR.next(); } pub fn abort() { INCR.next(); }
pub async fn highlight(&self, skip: usize, size: Size) -> Result<Text<'static>, PeekError> { pub async fn highlight(&self, skip: usize, size: Size) -> Result<Text<'static>, PeekError> {
let mut reader = BufReader::new(Local::open(&self.path).await?); let mut reader = BufReader::new(Local.open(&self.path).await?);
let syntax = Self::find_syntax(&self.path, &mut reader).await; let syntax = Self::find_syntax(&self.path, &mut reader).await;
let mut plain = syntax.is_err(); let mut plain = syntax.is_err();

View file

@ -3,7 +3,7 @@ use std::str::FromStr;
use mlua::{ExternalError, Function, IntoLua, IntoLuaMulti, Lua, Table, Value}; use mlua::{ExternalError, Function, IntoLua, IntoLuaMulti, Lua, Table, Value};
use yazi_binding::{Cha, Composer, ComposerGet, ComposerSet, Error, File, Url, UrlRef}; use yazi_binding::{Cha, Composer, ComposerGet, ComposerSet, Error, File, Url, UrlRef};
use yazi_config::Pattern; use yazi_config::Pattern;
use yazi_fs::{mounts::PARTITIONS, provider, remove_dir_clean}; use yazi_fs::{mounts::PARTITIONS, provider::{self, DirReader, FileHolder}, remove_dir_clean};
use yazi_shared::url::UrlCow; use yazi_shared::url::UrlCow;
use crate::bindings::SizeCalculator; use crate::bindings::SizeCalculator;
@ -121,7 +121,7 @@ fn read_dir(lua: &Lua) -> mlua::Result<Function> {
}; };
let mut files = vec![]; let mut files = vec![];
while let Ok(Some(next)) = it.next_entry().await { while let Ok(Some(next)) = it.next().await {
let url = next.url(); let url = next.url();
if pat.as_ref().is_some_and(|p| !p.match_url(&url, p.is_dir)) { if pat.as_ref().is_some_and(|p| !p.match_url(&url, p.is_dir)) {
continue; continue;

View file

@ -62,7 +62,7 @@ impl Loader {
let p = BOOT.plugin_dir.join(format!("{plugin}.yazi/{entry}.lua")); let p = BOOT.plugin_dir.join(format!("{plugin}.yazi/{entry}.lua"));
let chunk = let chunk =
Local::read(&p).await.with_context(|| format!("Failed to load plugin from {p:?}"))?.into(); Local.read(&p).await.with_context(|| format!("Failed to load plugin from {p:?}"))?.into();
let result = Self::compatible_or_error(id, &chunk); let result = Self::compatible_or_error(id, &chunk);
let inspect = f(&chunk); let inspect = f(&chunk);

View file

@ -4,7 +4,7 @@ use anyhow::{Result, anyhow};
use tokio::{io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; use tokio::{io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc};
use tracing::warn; use tracing::warn;
use yazi_config::YAZI; use yazi_config::YAZI;
use yazi_fs::{cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, path::{path_relative_to, skip_url}, provider::{self, DirEntry}}; use yazi_fs::{cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, path::{path_relative_to, skip_url}, provider::{self, DirEntry, DirReader, FileHolder}};
use yazi_shared::url::{Url, UrlBuf, UrlCow}; use yazi_shared::url::{Url, UrlBuf, UrlCow};
use super::{FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash}; use super::{FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash};
@ -69,7 +69,7 @@ impl File {
}); });
let mut it = continue_unless_ok!(provider::read_dir(&src).await); let mut it = continue_unless_ok!(provider::read_dir(&src).await);
while let Ok(Some(entry)) = it.next_entry().await { while let Ok(Some(entry)) = it.next().await {
let from = entry.url(); let from = entry.url();
let cha = continue_unless_ok!(Self::entry_cha(entry, &from, task.follow).await); let cha = continue_unless_ok!(Self::entry_cha(entry, &from, task.follow).await);
@ -203,7 +203,7 @@ impl File {
}); });
let mut it = continue_unless_ok!(provider::read_dir(&src).await); let mut it = continue_unless_ok!(provider::read_dir(&src).await);
while let Ok(Some(entry)) = it.next_entry().await { while let Ok(Some(entry)) = it.next().await {
let from = entry.url(); let from = entry.url();
let cha = continue_unless_ok!(Self::entry_cha(entry, &from, task.follow).await); let cha = continue_unless_ok!(Self::entry_cha(entry, &from, task.follow).await);
@ -251,7 +251,7 @@ impl File {
while let Some(target) = dirs.pop_front() { while let Some(target) = dirs.pop_front() {
let Ok(mut it) = provider::read_dir(&target).await else { continue }; let Ok(mut it) = provider::read_dir(&target).await else { continue };
while let Ok(Some(entry)) = it.next_entry().await { while let Ok(Some(entry)) = it.next().await {
let Ok(cha) = entry.metadata().await else { continue }; let Ok(cha) = entry.metadata().await else { continue };
if cha.is_dir() { if cha.is_dir() {

View file

@ -1,15 +1,15 @@
use std::{borrow::Cow, ffi::OsStr, path::PathBuf}; use std::{borrow::Cow, ffi::OsStr, path::PathBuf, sync::Arc};
use crate::{ByteStr, fs::Attrs}; use crate::{ByteStr, fs::Attrs};
pub struct DirEntry<'a> { pub struct DirEntry {
pub(super) dir: ByteStr<'a>, pub(super) dir: Arc<ByteStr<'static>>,
pub(super) name: ByteStr<'a>, pub(super) name: ByteStr<'static>,
pub(super) long_name: ByteStr<'a>, pub(super) long_name: ByteStr<'static>,
pub(super) attrs: Attrs, pub(super) attrs: Attrs,
} }
impl<'a> DirEntry<'a> { impl DirEntry {
#[must_use] #[must_use]
pub fn path(&self) -> PathBuf { self.dir.join(&self.name) } pub fn path(&self) -> PathBuf { self.dir.join(&self.name) }

View file

@ -4,7 +4,7 @@ use crate::{ByteStr, Error, Session, fs::DirEntry, requests, responses};
pub struct ReadDir { pub struct ReadDir {
session: Arc<Session>, session: Arc<Session>,
dir: ByteStr<'static>, dir: Arc<ByteStr<'static>>,
handle: String, handle: String,
name: responses::Name<'static>, name: responses::Name<'static>,
@ -16,7 +16,7 @@ impl ReadDir {
pub(crate) fn new(session: &Arc<Session>, dir: ByteStr, handle: String) -> Self { pub(crate) fn new(session: &Arc<Session>, dir: ByteStr, handle: String) -> Self {
Self { Self {
session: session.clone(), session: session.clone(),
dir: dir.into_owned(), dir: Arc::new(dir.into_owned()),
handle, handle,
name: Default::default(), name: Default::default(),
@ -25,7 +25,7 @@ impl ReadDir {
} }
} }
pub async fn next(&mut self) -> Result<Option<DirEntry<'_>>, Error> { pub async fn next(&mut self) -> Result<Option<DirEntry>, Error> {
loop { loop {
self.fetch().await?; self.fetch().await?;
let Some(item) = self.name.items.get_mut(self.cursor).map(mem::take) else { let Some(item) = self.name.items.get_mut(self.cursor).map(mem::take) else {
@ -35,7 +35,7 @@ impl ReadDir {
self.cursor += 1; self.cursor += 1;
if item.name != "." && item.name != ".." { if item.name != "." && item.name != ".." {
return Ok(Some(DirEntry { return Ok(Some(DirEntry {
dir: ByteStr::from(&self.dir), dir: self.dir.clone(),
name: item.name, name: item.name,
long_name: item.long_name, long_name: item.long_name,
attrs: item.attrs, attrs: item.attrs,

View file

@ -1,5 +1,6 @@
use std::{ops::Deref, path::PathBuf, sync::Arc}; use std::{ops::Deref, path::PathBuf, sync::Arc};
use russh::{ChannelStream, client::Msg};
use tokio::sync::oneshot; use tokio::sync::oneshot;
use crate::{ByteStr, Error, Packet, Session, fs::{Attrs, File, Flags, ReadDir}, requests, responses}; use crate::{ByteStr, Error, Packet, Session, fs::{Attrs, File, Flags, ReadDir}, requests, responses};
@ -17,6 +18,8 @@ impl From<&Arc<Session>> for Operator {
} }
impl Operator { impl Operator {
pub fn make(stream: ChannelStream<Msg>) -> Self { Self(Session::make(stream)) }
pub async fn init(&mut self) -> Result<(), Error> { pub async fn init(&mut self) -> Result<(), Error> {
let version: responses::Version = self.send(requests::Init::default()).await?; let version: responses::Version = self.send(requests::Init::default()).await?;
*self.extensions.lock() = version.extensions; *self.extensions.lock() = version.extensions;

View file

@ -42,7 +42,7 @@ impl ExtendedData for ExtendedFsync<'_> {
} }
// --- Hardlink // --- Hardlink
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct ExtendedHardlink<'a> { pub struct ExtendedHardlink<'a> {
pub original: ByteStr<'a>, pub original: ByteStr<'a>,
pub link: ByteStr<'a>, pub link: ByteStr<'a>,

View file

@ -2,7 +2,7 @@ use std::borrow::Cow;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Fstat<'a> { pub struct Fstat<'a> {
pub id: u32, pub id: u32,
pub handle: Cow<'a, str>, pub handle: Cow<'a, str>,

View file

@ -19,7 +19,7 @@ impl<'a> SetStat<'a> {
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() + self.attrs.len() } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() + self.attrs.len() }
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct FSetStat<'a> { pub struct FSetStat<'a> {
pub id: u32, pub id: u32,
pub handle: Cow<'a, str>, pub handle: Cow<'a, str>,

View file

@ -15,7 +15,7 @@ pub struct Session {
} }
impl Session { impl Session {
pub fn make(stream: ChannelStream<Msg>) -> Arc<Self> { pub(super) fn make(stream: ChannelStream<Msg>) -> Arc<Self> {
let (tx, mut rx) = mpsc::unbounded_channel(); let (tx, mut rx) = mpsc::unbounded_channel();
let me = Arc::new(Self { let me = Arc::new(Self {
tx, tx,

View file

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize, de};
use crate::{Id, SStr, url::{UrlBuf, UrlCow, UrnBuf}}; use crate::{Id, SStr, url::{UrlBuf, UrlCow, UrnBuf}};
// --- Data // --- Data
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum Data { pub enum Data {
Nil, Nil,
@ -154,7 +154,7 @@ impl PartialEq<bool> for Data {
} }
// --- Key // --- Key
#[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum DataKey { pub enum DataKey {
Nil, Nil,

View file

@ -3,7 +3,7 @@ use std::{fmt::Display, str::FromStr, sync::atomic::{AtomicU64, Ordering}};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive( #[derive(
Clone, Copy, Debug, Default, Hash, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize, Clone, Copy, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize,
)] )]
pub struct Id(pub u64); pub struct Id(pub u64);

24
yazi-vfs/Cargo.toml Normal file
View file

@ -0,0 +1,24 @@
[package]
name = "yazi-vfs"
version = "25.9.15"
edition = "2024"
license = "MIT"
authors = [ "sxyazi <sxyazi@gmail.com>" ]
description = "Yazi virtual file system"
homepage = "https://yazi-rs.github.io"
repository = "https://github.com/sxyazi/yazi"
[dependencies]
yazi-macro = { path = "../yazi-macro", version = "25.9.15" }
yazi-shared = { path = "../yazi-shared", version = "25.9.15" }
# External dependencies
anyhow = { workspace = true }
dirs = { workspace = true }
hashbrown = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
toml = { workspace = true }
[target."cfg(unix)".dependencies]
uzers = { workspace = true }

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(provider vfs);

View file

@ -0,0 +1,29 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum Provider {
Sftp(ProviderSftp),
}
impl TryFrom<&'static Provider> for &'static ProviderSftp {
type Error = &'static str;
fn try_from(value: &'static Provider) -> Result<Self, Self::Error> {
match value {
Provider::Sftp(p) => Ok(p),
}
}
}
// --- SFTP
#[derive(Deserialize, Hash, Serialize, Eq, PartialEq)]
pub struct ProviderSftp {
pub host: String,
pub user: String,
pub port: u16,
pub password: Option<String>,
pub key_file: Option<PathBuf>,
}

View file

@ -0,0 +1,61 @@
use std::io;
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use tokio::sync::OnceCell;
use yazi_macro::ok_or_not_found;
use super::Provider;
use crate::local::Xdg;
#[derive(Deserialize, Serialize)]
pub struct Vfs {
pub providers: HashMap<String, Provider>,
}
impl Vfs {
pub async fn load() -> io::Result<&'static Self> {
pub static LOADED: OnceCell<Vfs> = OnceCell::const_new();
async fn init() -> io::Result<Vfs> {
tokio::task::spawn_blocking(|| {
toml::from_str::<Vfs>(&Vfs::read()?).map_err(io::Error::other)?.reshape()
})
.await?
}
LOADED.get_or_try_init(init).await
}
pub async fn provider<'a, P>(name: &str) -> io::Result<(&'a str, P)>
where
P: TryFrom<&'a Provider, 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}")));
};
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}"))),
}
}
pub(crate) fn read() -> io::Result<String> {
let p = Xdg::config_dir().join("vfs.toml");
Ok(ok_or_not_found!(std::fs::read_to_string(&p).map_err(|e| {
std::io::Error::new(e.kind(), format!("Failed to read VFS config {p:?}: {e}"))
})))
}
pub(crate) fn reshape(self) -> io::Result<Self> {
for name in self.providers.keys() {
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")))?;
}
}
Ok(self)
}
}

1
yazi-vfs/src/lib.rs Normal file
View file

@ -0,0 +1 @@
yazi_macro::mod_pub!(config local);

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(xdg);