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

View file

@ -1,7 +1,7 @@
use std::{ffi::OsString, mem, path::MAIN_SEPARATOR_STR};
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_parser::cmp::{CmpItem, ShowOpt, TriggerOpt};
use yazi_proxy::CmpProxy;
@ -45,7 +45,7 @@ impl Actor for Trigger {
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 {
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())
.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![
OsString::new(),
tmp.to_owned().into(),
@ -60,7 +60,8 @@ impl Actor for BulkRename {
defer!(AppProxy::resume());
AppProxy::stop().await;
let new: Vec<_> = Local::read_to_string(&tmp)
let new: Vec<_> = Local
.read_to_string(&tmp)
.await?
.lines()
.take(old.len())

View file

@ -39,7 +39,7 @@ impl Image {
})
.await??;
Ok(Local::write(cache, buf).await?)
Ok(Local.write(cache, buf).await?)
}
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-macro = { path = "../yazi-macro", version = "25.9.15" }
yazi-shared = { path = "../yazi-shared", version = "25.9.15" }
yazi-vfs = { path = "../yazi-vfs", version = "25.9.15" }
# External dependencies
clap = { workspace = true }

View file

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

View file

@ -3,8 +3,9 @@ use std::path::PathBuf;
use futures::executor::block_on;
use hashbrown::HashSet;
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_vfs::local::Xdg;
#[derive(Debug, Default, Serialize)]
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-macro = { path = "../yazi-macro", version = "25.9.15" }
yazi-shared = { path = "../yazi-shared", version = "25.9.15" }
yazi-vfs = { path = "../yazi-vfs", version = "25.9.15" }
# External dependencies
anyhow = { workspace = true }

View file

@ -23,7 +23,7 @@ impl Dependency {
pub(super) async fn delete_assets(&self) -> Result<()> {
let assets = self.target().join("assets");
match Local::read_dir(&assets).await {
match Local.read_dir(&assets).await {
Ok(mut it) => {
while let Some(entry) = it.next().await? {
remove_sealed(&entry.path())
@ -49,7 +49,7 @@ impl Dependency {
.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!")?;
} else {
outln!(

View file

@ -3,8 +3,9 @@ use std::{io::BufWriter, path::{Path, PathBuf}, str::FromStr};
use anyhow::{Result, bail};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
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_vfs::local::Xdg;
#[derive(Clone, Default)]
pub(crate) struct Dependency {
@ -62,7 +63,7 @@ impl Dependency {
}
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> =
["LICENSE", "README.md", "main.lua"].into_iter().map(Into::into).collect();
while let Some(entry) = it.next().await? {

View file

@ -20,7 +20,7 @@ impl Dependency {
self.hash_check().await?;
}
Local::create_dir_all(&to).await?;
Local.create_dir_all(&to).await?;
self.delete_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<()> {
match Local::read_dir(&from).await {
match Local.read_dir(&from).await {
Ok(mut it) => {
Local::create_dir_all(&to).await?;
Local.create_dir_all(&to).await?;
while let Some(entry) = it.next().await? {
let (src, dist) = (entry.path(), to.join(entry.name()));
copy_and_seal(&src, &dist).await.with_context(|| {

View file

@ -14,17 +14,17 @@ impl Dependency {
for file in files {
h.write(file.as_bytes());
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![];
match Local::read_dir(dir.join("assets")).await {
match Local.read_dir(dir.join("assets")).await {
Ok(mut it) => {
while let Some(entry) = it.next().await? {
let Ok(name) = entry.name().into_owned().into_string() else {
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 => {}

View file

@ -3,7 +3,7 @@
yazi_macro::mod_flat!(add delete dependency deploy git hash install package upgrade);
use anyhow::Context;
use yazi_fs::Xdg;
use yazi_vfs::local::Xdg;
pub(super) fn init() -> anyhow::Result<()> {
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 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_vfs::local::Xdg;
use super::Dependency;
@ -15,7 +16,7 @@ pub(crate) struct Package {
impl Package {
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)?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Self::default(),
Err(e) => Err(e)?,
@ -135,7 +136,7 @@ impl Package {
async fn save(&self) -> Result<()> {
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") }

View file

@ -5,19 +5,19 @@ use yazi_fs::{ok_or_not_found, provider::{FileBuilder, Provider, local::{Gate, L
#[inline]
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]
pub async fn maybe_exists(path: impl AsRef<Path>) -> bool {
match Local::symlink_metadata(path).await {
match Local.symlink_metadata(path).await {
Ok(_) => true,
Err(e) => e.kind() != std::io::ErrorKind::NotFound,
}
}
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)?;
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?;
}
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-shared = { path = "../yazi-shared", version = "25.9.15" }
yazi-term = { path = "../yazi-term", version = "25.9.15" }
yazi-vfs = { path = "../yazi-vfs", version = "25.9.15" }
# External dependencies
anyhow = { workspace = true }
@ -26,6 +27,7 @@ indexmap = { workspace = true }
ratatui = { workspace = true }
regex = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }

View file

@ -1,8 +1,9 @@
use anyhow::{Context, Result};
use serde::Deserialize;
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_vfs::local::Xdg;
use super::{Chord, KeymapRules};

View file

@ -1,6 +1,6 @@
#![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);

View file

@ -3,8 +3,9 @@ use std::{borrow::Cow, path::PathBuf};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
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_vfs::local::Xdg;
use super::PreviewWrap;

View file

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

View file

@ -3,8 +3,9 @@ use std::path::PathBuf;
use anyhow::{Context, Result, anyhow, bail};
use serde::Deserialize;
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_vfs::local::Xdg;
use super::{Filetype, Flavor, Icon};
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 serde::Deserialize;
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};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -58,7 +58,7 @@ impl State {
return Ok(());
}
Local::create_dir_all(&BOOT.state_dir).await?;
Local.create_dir_all(&BOOT.state_dir).await?;
let mut buf = BufWriter::new(
Gate::default()
.write(true)
@ -79,7 +79,7 @@ impl State {
}
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 inner = HashMap::new();
@ -103,7 +103,7 @@ impl State {
}
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();
Ok(modified >= self.last.load(Ordering::Relaxed) as u128)
}

View file

@ -40,7 +40,7 @@ impl Stream {
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)
}

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-shared = { path = "../yazi-shared", 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-widgets = { path = "../yazi-widgets", version = "25.9.15" }

View file

@ -26,13 +26,13 @@ impl App {
async fn cwd_to_file(&self, no: bool) {
if let Some(p) = ARGS.cwd_file.as_ref().filter(|_| !no) {
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>) {
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 tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::EnvFilter;
use yazi_fs::Xdg;
use yazi_shared::{LOG_LEVEL, RoCell};
use yazi_vfs::local::Xdg;
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-sftp = { path = "../yazi-sftp", version = "0.1.0" }
yazi-shared = { path = "../yazi-shared", version = "25.9.15" }
yazi-vfs = { path = "../yazi-vfs", version = "25.9.15" }
# External dependencies
anyhow = { workspace = true }
@ -25,6 +26,7 @@ futures = { workspace = true }
hashbrown = { workspace = true }
parking_lot = { workspace = true }
regex = { workspace = true }
russh = { workspace = true }
scopeguard = { workspace = true }
serde = { 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 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)]
pub struct Files {
@ -40,7 +40,7 @@ impl Files {
let (tx, rx) = mpsc::unbounded_channel();
tokio::spawn(async move {
while let Ok(Some(ent)) = it.next_entry().await {
while let Ok(Some(ent)) = it.next().await {
select! {
_ = tx.closed() => break,
result = ent.metadata() => {
@ -59,7 +59,7 @@ impl Files {
pub async fn from_dir_bulk(dir: &UrlBuf) -> std::io::Result<Vec<File>> {
let mut it = provider::read_dir(dir).await?;
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);
}

View file

@ -2,7 +2,7 @@ use anyhow::Result;
use tokio::{io, select, sync::{mpsc, oneshot}, time};
use yazi_shared::url::{Component, Url, UrlBuf};
use crate::{cha::Cha, provider};
use crate::{cha::Cha, provider::{self, DirReader, FileHolder}};
#[inline]
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) {
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()) {
let url = ent.url();
Box::pin(remove_dir_clean(&url)).await;

View file

@ -2,7 +2,7 @@
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() {
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 crate::provider::{self, ReadDir};
use crate::provider::{self, DirReader, FileHolder, ReadDir};
pub enum SizeCalculator {
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!();
};

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 crate::{cha::{Cha, ChaType}, provider::FileHolder};
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 {
fn from(value: super::local::DirEntry) -> Self { Self::Local(value) }
impl FileHolder for DirEntry {
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 {
#[must_use]
pub fn url(&self) -> UrlBuf {
match self {
Self::Local(local) => local.path().into(),
}
}
#[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,
Self::Regular(ent) => ent.path().into(),
Self::Search((dir, ent)) => dir.join(ent.name()),
Self::Sftp((dir, ent)) => dir.join(ent.name()),
}
}
}

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 yazi_shared::scheme::SchemeRef;
use crate::provider::FileBuilder;
#[derive(Default)]
@ -23,6 +25,14 @@ impl FileBuilder for Gate {
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>
where
P: AsRef<Path>,

View file

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

View file

@ -5,9 +5,9 @@ use crate::provider::DirReader;
pub struct ReadDir(pub(super) tokio::fs::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))
}
}

View file

@ -1,5 +1,5 @@
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(); }

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}}};
@ -9,7 +9,7 @@ pub fn cache<'a, U>(url: U) -> Option<PathBuf>
where
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]
@ -31,7 +31,7 @@ where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::canonicalize(path).await.map(Into::into)
Local.canonicalize(path).await.map(Into::into)
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
@ -56,7 +56,7 @@ where
V: Into<Url<'a>>,
{
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 {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
@ -68,7 +68,7 @@ where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::create(path).await.map(Into::into)
Local.create(path).await.map(Into::into)
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
@ -80,7 +80,7 @@ where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::create_dir(path).await
Local.create_dir(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
@ -92,7 +92,7 @@ where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::create_dir_all(path).await
Local.create_dir_all(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
@ -105,7 +105,7 @@ where
V: Into<Url<'a>>,
{
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 {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
@ -130,7 +130,7 @@ where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::metadata(path).await
Local.metadata(path).await
} else {
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
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::read_dir(path).await.map(Into::into)
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
let url: Url = url.into();
Ok(match url.scheme {
SchemeRef::Regular => ReadDir::Regular(Local.read_dir(url.loc).await?),
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]
@ -163,7 +169,7 @@ where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::read_link(path).await
Local.read_link(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
@ -175,7 +181,7 @@ where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::remove_dir(path).await
Local.remove_dir(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
@ -187,7 +193,7 @@ where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::remove_dir_all(path).await
Local.remove_dir_all(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
@ -199,7 +205,7 @@ where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::remove_file(path).await
Local.remove_file(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
@ -212,7 +218,7 @@ where
V: Into<Url<'a>>,
{
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 {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
@ -225,7 +231,7 @@ where
F: AsyncFnOnce() -> io::Result<bool>,
{
if let Some(link) = link.into().as_path() {
Local::symlink(original, link, is_dir).await
Local.symlink(original, link, is_dir).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
@ -237,7 +243,7 @@ where
U: Into<Url<'a>>,
{
if let Some(link) = link.into().as_path() {
Local::symlink_dir(original, link).await
Local.symlink_dir(original, link).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
@ -249,7 +255,7 @@ where
U: Into<Url<'a>>,
{
if let Some(link) = link.into().as_path() {
Local::symlink_file(original, link).await
Local.symlink_file(original, link).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
@ -261,7 +267,7 @@ where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::symlink_metadata(path).await
Local.symlink_metadata(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
@ -273,7 +279,7 @@ where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local::trash(path).await
Local.trash(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
@ -286,7 +292,7 @@ where
C: AsRef<[u8]>,
{
if let Some(path) = url.into().as_path() {
Local::write(path, contents).await
Local.write(path, contents).await
} else {
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;
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 {
fn from(value: super::local::ReadDir) -> Self { Self::Local(value) }
}
impl DirReader for ReadDir {
type Entry = super::DirEntry;
impl ReadDir {
pub async fn next_entry(&mut self) -> io::Result<Option<DirEntry>> {
match self {
Self::Local(local) => local.next().await.map(|entry| entry.map(Into::into)),
}
async fn next(&mut self) -> io::Result<Option<Self::Entry>> {
Ok(match self {
Self::Regular(reader) => reader.next().await?.map(Self::Entry::Regular),
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 {
Tokio(tokio::fs::File),
SFTP(Box<yazi_sftp::fs::File>),
Sftp(Box<yazi_sftp::fs::File>),
}
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 {
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 {
@ -24,7 +24,7 @@ impl AsyncRead for RwFile {
) -> std::task::Poll<std::io::Result<()>> {
match &mut *self {
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>> {
match &mut *self {
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>> {
match &mut *self {
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>> {
match &mut *self {
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>> {
match &mut *self {
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 {
match self {
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 yazi_sftp::fs::{Attrs, Flags};
use yazi_shared::scheme::SchemeRef;
use yazi_vfs::config::{ProviderSftp, Vfs};
use crate::provider::FileBuilder;
#[derive(Default)]
pub struct Gate {
sftp: super::Sftp,
append: bool,
create: bool,
create_new: bool,
@ -32,6 +35,24 @@ impl FileBuilder for Gate {
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>
where
P: AsRef<Path>,
@ -55,7 +76,8 @@ impl FileBuilder for Gate {
if self.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 {

View file

@ -2,10 +2,10 @@ use std::{ffi::OsStr, io, time::{Duration, UNIX_EPOCH}};
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;
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()))?;
cha.nlink = ent.nlink().unwrap_or_default();
Ok(cha)

View file

@ -1,6 +1,12 @@
yazi_macro::mod_flat!(gate metadata read_dir sftp);
pub(super) static CONN: yazi_shared::RoCell<deadpool::managed::Pool<Sftp>> =
yazi_shared::RoCell::new();
pub(super) static CONN: yazi_shared::RoCell<
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);
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))
}
}
// --- 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 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_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 {
type File = yazi_sftp::fs::File;
type Gate = super::Gate;
type ReadDir = super::ReadDir;
fn cache<P>(_: P) -> Option<PathBuf>
fn cache<P>(&self, _: P) -> Option<PathBuf>
where
P: AsRef<Path>,
{
todo!()
}
async fn canonicalize<P>(path: P) -> io::Result<PathBuf>
async fn canonicalize<P>(&self, path: P) -> io::Result<PathBuf>
where
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
P: AsRef<Path>,
Q: AsRef<Path>,
{
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 to = op.open(&to, Flags::WRITE | Flags::CREATE | Flags::TRUNCATE, attrs).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
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
P: 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
P: AsRef<Path>,
{
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()
}
async fn read_dir<P>(path: P) -> io::Result<Self::ReadDir>
async fn read_dir<P>(&self, path: P) -> io::Result<Self::ReadDir>
where
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
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
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
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
P: 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
P: AsRef<Path>,
Q: AsRef<Path>,
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
P: AsRef<Path>,
{
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()
}
async fn trash<P>(_path: P) -> io::Result<()>
async fn trash<P>(&self, _path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
@ -126,10 +140,14 @@ impl Provider for 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;
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::Backend(e) => e,
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 {
type Error = io::Error;
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(
&self,
obj: &mut Self::Type,
metrics: &deadpool::managed::Metrics,
) -> 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 yazi_macro::ok_or_not_found;
use yazi_shared::scheme::SchemeRef;
use crate::cha::{Cha, ChaType};
@ -10,31 +11,31 @@ pub trait Provider {
type Gate: FileBuilder<File = Self::File>;
type ReadDir: DirReader;
fn cache<P>(_: P) -> Option<PathBuf>
fn cache<P>(&self, path: P) -> Option<PathBuf>
where
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
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
P: 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
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
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
P: AsRef<Path>,
{
@ -44,147 +45,154 @@ pub trait Provider {
return Ok(());
}
match Self::create_dir(path).await {
match self.create_dir(path).await {
Ok(()) => return Ok(()),
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),
}
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")),
}
match Self::create_dir(path).await {
match self.create_dir(path).await {
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),
}
}
}
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
P: 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
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
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
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
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
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
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
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? {
let ft = ok_or_not_found!(child.file_type().await, continue);
let result = if ft.is_dir() {
remove_dir_all_impl::<S>(&child.path()).await
remove_dir_all_impl(me, &child.path()).await
} else {
S::remove_file(&child.path()).await
me.remove_file(&child.path()).await
};
() = 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 {
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() {
Self::remove_file(path).await
self.remove_file(path).await
} 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
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
P: 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
P: AsRef<Path>,
Q: AsRef<Path>,
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
P: 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
P: 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
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
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
P: AsRef<Path>,
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
pub trait DirReader {
type Entry<'a>: FileHolder
where
Self: 'a;
type Entry: FileHolder;
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
pub trait FileHolder {
#[must_use]
fn path(&self) -> PathBuf;
#[must_use]
fn name(&self) -> Cow<'_, OsStr>;
fn metadata(&self) -> impl Future<Output = io::Result<Cha>>;
@ -193,7 +201,7 @@ pub trait FileHolder {
}
// --- FileOpener
pub trait FileBuilder: Default {
pub trait FileBuilder {
type File: AsyncRead + AsyncWrite + Unpin;
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 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>>
where
P: AsRef<Path>;

View file

@ -8,6 +8,6 @@ macro_rules! ok_or_not_found {
}
};
($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 yazi_shared::event::{CmdCow, Data, EventQuit};
#[derive(Debug, Default, Serialize, Deserialize)]
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct QuitOpt {
pub code: i32,
pub no_cwd_file: bool,

View file

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

View file

@ -39,7 +39,7 @@ impl Highlighter {
pub fn abort() { INCR.next(); }
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 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 yazi_binding::{Cha, Composer, ComposerGet, ComposerSet, Error, File, Url, UrlRef};
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 crate::bindings::SizeCalculator;
@ -121,7 +121,7 @@ fn read_dir(lua: &Lua) -> mlua::Result<Function> {
};
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();
if pat.as_ref().is_some_and(|p| !p.match_url(&url, p.is_dir)) {
continue;

View file

@ -62,7 +62,7 @@ impl Loader {
let p = BOOT.plugin_dir.join(format!("{plugin}.yazi/{entry}.lua"));
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 inspect = f(&chunk);

View file

@ -4,7 +4,7 @@ use anyhow::{Result, anyhow};
use tokio::{io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc};
use tracing::warn;
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 super::{FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash};
@ -69,7 +69,7 @@ impl File {
});
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 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);
while let Ok(Some(entry)) = it.next_entry().await {
while let Ok(Some(entry)) = it.next().await {
let from = entry.url();
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() {
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 };
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};
pub struct DirEntry<'a> {
pub(super) dir: ByteStr<'a>,
pub(super) name: ByteStr<'a>,
pub(super) long_name: ByteStr<'a>,
pub struct DirEntry {
pub(super) dir: Arc<ByteStr<'static>>,
pub(super) name: ByteStr<'static>,
pub(super) long_name: ByteStr<'static>,
pub(super) attrs: Attrs,
}
impl<'a> DirEntry<'a> {
impl DirEntry {
#[must_use]
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 {
session: Arc<Session>,
dir: ByteStr<'static>,
dir: Arc<ByteStr<'static>>,
handle: String,
name: responses::Name<'static>,
@ -16,7 +16,7 @@ impl ReadDir {
pub(crate) fn new(session: &Arc<Session>, dir: ByteStr, handle: String) -> Self {
Self {
session: session.clone(),
dir: dir.into_owned(),
dir: Arc::new(dir.into_owned()),
handle,
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 {
self.fetch().await?;
let Some(item) = self.name.items.get_mut(self.cursor).map(mem::take) else {
@ -35,7 +35,7 @@ impl ReadDir {
self.cursor += 1;
if item.name != "." && item.name != ".." {
return Ok(Some(DirEntry {
dir: ByteStr::from(&self.dir),
dir: self.dir.clone(),
name: item.name,
long_name: item.long_name,
attrs: item.attrs,

View file

@ -1,5 +1,6 @@
use std::{ops::Deref, path::PathBuf, sync::Arc};
use russh::{ChannelStream, client::Msg};
use tokio::sync::oneshot;
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 {
pub fn make(stream: ChannelStream<Msg>) -> Self { Self(Session::make(stream)) }
pub async fn init(&mut self) -> Result<(), Error> {
let version: responses::Version = self.send(requests::Init::default()).await?;
*self.extensions.lock() = version.extensions;

View file

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

View file

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

View file

@ -15,7 +15,7 @@ pub struct 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 me = Arc::new(Self {
tx,

View file

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

View file

@ -3,7 +3,7 @@ use std::{fmt::Display, str::FromStr, sync::atomic::{AtomicU64, Ordering}};
use serde::{Deserialize, Serialize};
#[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);

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);