feat: SFTP connection pool (#3198)

This commit is contained in:
三咲雅 misaki masa 2025-09-24 19:49:37 +08:00 committed by GitHub
parent 9a804bac9c
commit 86f0885737
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 570 additions and 337 deletions

92
Cargo.lock generated
View file

@ -150,9 +150,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.99"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "approx"
@ -459,9 +459,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.37"
version = "1.2.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44"
checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9"
dependencies = [
"find-msvc-tools",
"jobserver",
@ -527,9 +527,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.47"
version = "4.5.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae"
dependencies = [
"clap_builder",
"clap_derive",
@ -537,9 +537,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.47"
version = "4.5.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9"
dependencies = [
"anstream",
"anstyle",
@ -919,9 +919,9 @@ dependencies = [
[[package]]
name = "deranged"
version = "0.5.3"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc"
checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071"
dependencies = [
"powerfmt",
]
@ -1243,9 +1243,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959"
[[package]]
name = "flate2"
@ -1613,18 +1613,18 @@ dependencies = [
[[package]]
name = "imgref"
version = "1.11.0"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
[[package]]
name = "indexmap"
version = "2.11.3"
version = "2.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92119844f513ffa41556430369ab02c295a3578af21cf945caa3e9e0c2481ac3"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
"hashbrown 0.15.5",
"hashbrown 0.16.0",
"serde",
"serde_core",
]
@ -1824,9 +1824,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]]
name = "libc"
version = "0.2.175"
version = "0.2.176"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
[[package]]
name = "libfuzzer-sys"
@ -2685,9 +2685,9 @@ dependencies = [
[[package]]
name = "pxfm"
version = "0.1.23"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55f4fedc84ed39cb7a489322318976425e42a147e2be79d8f878e2884f94e84"
checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde"
dependencies = [
"num-traits",
]
@ -3203,9 +3203,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde"
version = "1.0.225"
version = "1.0.226"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d"
checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd"
dependencies = [
"serde_core",
"serde_derive",
@ -3223,18 +3223,18 @@ dependencies = [
[[package]]
name = "serde_core"
version = "1.0.225"
version = "1.0.226"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383"
checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.225"
version = "1.0.226"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516"
checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33"
dependencies = [
"proc-macro2",
"quote",
@ -3265,9 +3265,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2789234a13a53fc4be1b51ea1bab45a3c338bdb884862a257d10e5a74ae009e6"
checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee"
dependencies = [
"serde_core",
]
@ -3617,11 +3617,12 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.43"
version = "0.3.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031"
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
@ -3716,14 +3717,14 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.6"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae2a4cf385da23d1d53bc15cdfa5c2109e93d8d362393c801e87da2f72f0e201"
checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned 1.0.1",
"toml_datetime 0.7.1",
"serde_spanned 1.0.2",
"toml_datetime 0.7.2",
"toml_parser",
"toml_writer",
"winnow",
@ -3740,9 +3741,9 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.7.1"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a197c0ec7d131bfc6f7e82c8442ba1595aeab35da7adbf05b6b73cd06a16b6be"
checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1"
dependencies = [
"serde_core",
]
@ -3762,18 +3763,18 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109"
[[package]]
name = "tracing"
@ -4695,7 +4696,7 @@ dependencies = [
"serde",
"serde_json",
"tokio",
"toml 0.9.6",
"toml 0.9.7",
"twox-hash",
"vergen-gitcl",
"yazi-boot",
@ -4728,8 +4729,7 @@ dependencies = [
"ratatui",
"regex",
"serde",
"tokio",
"toml 0.9.6",
"toml 0.9.7",
"tracing",
"yazi-codegen",
"yazi-fs",
@ -4868,6 +4868,7 @@ dependencies = [
"tokio",
"tracing",
"trash",
"twox-hash",
"uzers",
"windows-sys 0.61.0",
"yazi-ffi",
@ -5038,12 +5039,11 @@ dependencies = [
name = "yazi-vfs"
version = "25.9.15"
dependencies = [
"anyhow",
"dirs",
"hashbrown 0.16.0",
"serde",
"tokio",
"toml 0.9.6",
"toml 0.9.7",
"uzers",
"yazi-macro",
"yazi-shared",

View file

@ -21,10 +21,10 @@ lto = false
[workspace.dependencies]
ansi-to-tui = "7.0.0"
anyhow = "1.0.99"
anyhow = "1.0.100"
base64 = "0.22.1"
bitflags = { version = "2.9.4", features = [ "serde" ] }
clap = { version = "4.5.47", features = [ "derive" ] }
clap = { version = "4.5.48", features = [ "derive" ] }
core-foundation-sys = "0.8.7"
crossterm = { version = "0.29.0", features = [ "event-stream" ] }
dirs = "6.0.0"
@ -32,8 +32,8 @@ foldhash = "0.2.0"
futures = "0.3.31"
globset = "0.4.16"
hashbrown = { version = "0.16.0", features = [ "serde" ] }
indexmap = { version = "2.11.3", features = [ "serde" ] }
libc = "0.2.175"
indexmap = { version = "2.11.4", features = [ "serde" ] }
libc = "0.2.176"
lru = "0.16.1"
mlua = { version = "0.11.3", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] }
objc = "0.2.7"
@ -44,13 +44,13 @@ ratatui = { version = "0.29.0", features = [ "unstable-rendered-line
regex = "1.11.2"
russh = { version = "0.54.3", default-features = false, features = [ "ring", "rsa" ] }
scopeguard = "1.2.0"
serde = { version = "1.0.225", features = [ "derive" ] }
serde = { version = "1.0.226", features = [ "derive" ] }
serde_json = "1.0.145"
syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
tokio = { version = "1.47.1", features = [ "full" ] }
tokio-stream = "0.1.17"
tokio-util = "0.7.16"
toml = { version = "0.9.6" }
toml = { version = "0.9.7" }
tracing = { version = "0.1.41", features = [ "max_level_debug", "release_max_level_debug" ] }
unicode-width = { version = "0.2.0", default-features = false }
twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] }

View file

@ -4,7 +4,7 @@ use futures::executor::block_on;
use hashbrown::HashSet;
use serde::Serialize;
use yazi_fs::{CWD, path::expand_url, provider};
use yazi_shared::url::{UrlBuf, UrnBuf};
use yazi_shared::url::{UrlBuf, UrlCow, UrnBuf};
use yazi_vfs::local::Xdg;
#[derive(Debug, Default, Serialize)]
@ -27,7 +27,12 @@ impl Boot {
return (vec![CWD.load().as_ref().clone()], vec![UrnBuf::default()]);
}
async fn go(entry: UrlBuf) -> (UrlBuf, UrnBuf) {
async fn go(entry: &UrlBuf) -> (UrlBuf, UrnBuf) {
let mut entry = expand_url(entry);
if let Ok(u @ UrlCow::Owned { .. }) = provider::absolute(&entry).await {
entry = u.into_owned();
}
let Some((parent, child)) = entry.pair() else {
return (entry, UrnBuf::default());
};
@ -39,7 +44,7 @@ impl Boot {
}
}
futures::future::join_all(entries.iter().map(expand_url).map(go)).await.into_iter().unzip()
futures::future::join_all(entries.iter().map(go)).await.into_iter().unzip()
}
}

View file

@ -27,7 +27,6 @@ indexmap = { workspace = true }
ratatui = { workspace = true }
regex = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }

View file

@ -31,6 +31,7 @@ scopeguard = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
twox-hash = { workspace = true }
[target."cfg(unix)".dependencies]
libc = { workspace = true }

View file

@ -3,6 +3,8 @@ use std::{env::{current_dir, set_current_dir}, ops::Deref, path::PathBuf, sync::
use arc_swap::ArcSwap;
use yazi_shared::{RoCell, url::UrlBuf};
use crate::provider;
pub static CWD: RoCell<Cwd> = RoCell::new();
pub struct Cwd(ArcSwap<UrlBuf>);
@ -26,16 +28,20 @@ impl Default for Cwd {
}
impl Cwd {
pub fn path(&self) -> PathBuf {
let url = self.0.load();
provider::cache(url.as_ref()).unwrap_or_else(|| url.loc.to_path())
}
pub fn set(&self, url: &UrlBuf) -> bool {
if self.load().as_ref() == url {
if !url.is_absolute() {
return false;
} else if self.0.load().as_ref() == url {
return false;
}
self.store(Arc::new(url.clone()));
if let Some(p) = url.as_path() {
unsafe { std::env::set_var("PWD", p) };
Self::sync_cwd();
}
self.0.store(Arc::new(url.clone()));
Self::sync_cwd();
true
}
@ -47,17 +53,20 @@ impl Cwd {
}
tokio::task::spawn_blocking(move || {
if let Some(p) = CWD.load().as_path() {
_ = set_current_dir(p);
}
let path = CWD.path();
std::fs::create_dir_all(&path).ok();
_ = set_current_dir(&path);
let cur = current_dir().unwrap_or_default();
unsafe { std::env::set_var("PWD", path) }
SYNCING.store(false, Ordering::Relaxed);
if let Some(p) = CWD.load().as_path()
&& cur != p
{
set_current_dir(p).ok();
let path = CWD.path();
if cur != path {
std::fs::create_dir_all(&path).ok();
set_current_dir(&path).ok();
unsafe { std::env::set_var("PWD", path) }
}
});
}

View file

@ -62,11 +62,13 @@ fn expand_variables(p: &Path) -> Cow<'_, Path> {
}
}
fn absolute_url<'a>(url: Url<'a>) -> UrlCow<'a> {
let b = url.loc.as_os_str().as_encoded_bytes();
let local = !url.scheme.is_virtual();
pub fn absolute_url<'a>(url: Url<'a>) -> UrlCow<'a> {
if url.scheme.is_virtual() {
return url.into();
}
if cfg!(windows) && local && b.len() == 2 && b[1] == b':' && b[0].is_ascii_alphabetic() {
let b = url.loc.as_os_str().as_encoded_bytes();
if cfg!(windows) && b.len() == 2 && b[1] == b':' && b[0].is_ascii_alphabetic() {
let loc = LocBuf::with(
format!(r"{}:\", b[0].to_ascii_uppercase() as char).into(),
if url.has_base() { 0 } else { 2 },
@ -74,8 +76,7 @@ fn absolute_url<'a>(url: Url<'a>) -> UrlCow<'a> {
)
.expect("Failed to create Loc from drive letter");
UrlBuf { loc, scheme: url.scheme.into() }.into()
} else if local
&& let Ok(rest) = url.loc.strip_prefix("~/")
} else if let Ok(rest) = url.loc.strip_prefix("~/")
&& let Some(home) = dirs::home_dir()
&& home.is_absolute()
{
@ -88,10 +89,10 @@ fn absolute_url<'a>(url: Url<'a>) -> UrlCow<'a> {
.expect("Failed to create Loc from home directory");
UrlBuf { loc, scheme: url.scheme.into() }.into()
} else if !url.is_absolute() {
let cwd = CWD.load();
let loc = LocBuf::with(cwd.loc.join(url.loc), url.uri().count(), url.urn().count())
let cwd = CWD.path();
let loc = LocBuf::with(cwd.join(url.loc), url.uri().count(), url.urn().count())
.expect("Failed to create Loc from relative path");
UrlBuf { loc, scheme: cwd.scheme.clone() }.into()
UrlBuf { loc, scheme: url.scheme.into() }.into()
} else {
url.into()
}

View file

@ -2,7 +2,7 @@ use std::{io, path::Path};
use yazi_shared::scheme::SchemeRef;
use crate::provider::FileBuilder;
use crate::{cha::Cha, provider::FileBuilder};
pub enum Gate {
Local(super::local::Gate),
@ -28,6 +28,14 @@ impl FileBuilder for Gate {
self
}
fn cha(&mut self, cha: Cha) -> &mut Self {
match self {
Self::Local(g) => _ = g.cha(cha),
Self::Sftp(g) => _ = g.cha(cha),
};
self
}
fn create(&mut self, create: bool) -> &mut Self {
match self {
Self::Local(g) => _ = g.create(create),

View file

@ -2,7 +2,7 @@ use std::{io, path::Path};
use yazi_shared::scheme::SchemeRef;
use crate::provider::FileBuilder;
use crate::{cha::Cha, provider::FileBuilder};
#[derive(Default)]
pub struct Gate(tokio::fs::OpenOptions);
@ -15,6 +15,12 @@ impl FileBuilder for Gate {
self
}
fn cha(&mut self, cha: Cha) -> &mut Self {
#[cfg(unix)]
self.0.mode(cha.mode.bits() as _);
self
}
fn create(&mut self, create: bool) -> &mut Self {
self.0.create(create);
self

View file

@ -1,6 +1,8 @@
use std::{io, path::{Path, PathBuf}};
use crate::{cha::Cha, provider::Provider};
use yazi_shared::url::{Url, UrlCow};
use crate::{cha::Cha, path::absolute_url, provider::Provider};
#[derive(Clone, Copy)]
pub struct Local;
@ -10,12 +12,16 @@ impl Provider for Local {
type Gate = super::Gate;
type ReadDir = super::ReadDir;
#[inline]
fn cache<P>(&self, _: P) -> Option<PathBuf>
async fn absolute<'a, U>(&self, url: U) -> io::Result<UrlCow<'a>>
where
P: AsRef<Path>,
U: Into<Url<'a>>,
{
None
let url: Url = url.into();
if url.scheme.is_virtual() {
Err(io::Error::new(io::ErrorKind::InvalidInput, "Not a local URL"))
} else {
Ok(absolute_url(url))
}
}
#[inline]
@ -220,23 +226,9 @@ impl Provider for Local {
impl Local {
async fn copy_impl(from: PathBuf, to: PathBuf, cha: Cha) -> io::Result<u64> {
let mut ft = std::fs::FileTimes::new();
cha.atime.map(|t| ft = ft.set_accessed(t));
cha.mtime.map(|t| ft = ft.set_modified(t));
#[cfg(target_os = "macos")]
{
use std::os::macos::fs::FileTimesExt;
cha.btime.map(|t| ft = ft.set_created(t));
}
#[cfg(windows)]
{
use std::os::windows::fs::FileTimesExt;
cha.btime.map(|t| ft = ft.set_created(t));
}
#[cfg(any(target_os = "linux", target_os = "android"))]
{
use std::os::{fd::AsRawFd, unix::fs::OpenOptionsExt};
use std::os::unix::fs::OpenOptionsExt;
tokio::task::spawn_blocking(move || {
let mut reader = std::fs::File::open(from)?;
@ -248,8 +240,8 @@ impl Local {
.open(to)?;
let written = std::io::copy(&mut reader, &mut writer)?;
unsafe { libc::fchmod(writer.as_raw_fd(), cha.mode.bits() as _) };
writer.set_times(ft).ok();
writer.set_permissions(cha.into()).ok();
writer.set_times(cha.into()).ok();
Ok(written)
})
@ -260,7 +252,11 @@ impl Local {
{
tokio::task::spawn_blocking(move || {
let written = std::fs::copy(from, &to)?;
std::fs::File::options().write(true).open(to).and_then(|f| f.set_times(ft)).ok();
if let Ok(file) = std::fs::File::options().write(true).open(to) {
file.set_times(cha.into()).ok();
}
Ok(written)
})
.await?

View file

@ -0,0 +1,40 @@
use std::fs::{FileTimes, Permissions};
use crate::cha::Cha;
impl From<Cha> for FileTimes {
fn from(cha: Cha) -> Self {
let mut t = std::fs::FileTimes::new();
if let Some(atime) = cha.atime {
t = t.set_accessed(atime);
}
#[cfg(target_os = "macos")]
if let Some(btime) = cha.btime {
use std::os::macos::fs::FileTimesExt;
t = t.set_created(btime);
}
#[cfg(windows)]
if let Some(btime) = cha.btime {
use std::os::windows::fs::FileTimesExt;
t = t.set_created(btime);
}
if let Some(mtime) = cha.mtime {
t = t.set_modified(mtime);
}
t
}
}
#[cfg(unix)]
impl From<Cha> for Permissions {
fn from(cha: Cha) -> Self {
use std::os::unix::fs::PermissionsExt;
Permissions::from_mode(cha.mode.bits() as _)
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(calculator casefold dir_entry gate identical local read_dir);
yazi_macro::mod_flat!(calculator casefold dir_entry gate identical local metadata read_dir);

View file

@ -1,18 +1,40 @@
use std::{io, path::{Path, PathBuf}, sync::Arc};
use std::{io, path::{Path, PathBuf}};
use yazi_shared::{scheme::SchemeRef, url::{Url, UrlBuf}};
use tokio::io::{BufReader, BufWriter};
use twox_hash::XxHash3_128;
use yazi_shared::{scheme::SchemeRef, url::{Url, UrlBuf, UrlCow}};
use yazi_vfs::local::Xdg;
use crate::{cha::Cha, provider::{Provider, ReadDir, RwFile, local::{self, Local}}};
use crate::{cha::Cha, provider::{Provider, Providers, ReadDir, RwFile, local::{self, Local}}};
pub async fn absolute<'a, U>(url: U) -> io::Result<UrlCow<'a>>
where
U: Into<Url<'a>>,
{
let url: Url = url.into();
Providers::new(url).await?.absolute(url).await
}
#[inline]
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 }
let url: Url = url.into();
match url.scheme {
SchemeRef::Regular | SchemeRef::Search(_) => None,
SchemeRef::Archive(name) => Some(
Xdg::cache_dir()
.join(format!("archive-{}", yazi_shared::url::Encode::domain(name)))
.join(format!("{:x}", XxHash3_128::oneshot(url.loc.bytes()))),
),
SchemeRef::Sftp(name) => Some(
Xdg::cache_dir()
.join(format!("sftp-{}", yazi_shared::url::Encode::domain(name)))
.join(format!("{:x}", XxHash3_128::oneshot(url.loc.bytes()))),
),
}
}
#[inline]
pub async fn calculate<'a, U>(url: U) -> io::Result<u64>
where
U: Into<Url<'a>>,
@ -25,19 +47,22 @@ where
}
}
#[inline]
pub async fn canonicalize<'a, U>(url: U) -> io::Result<UrlBuf>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local.canonicalize(path).await.map(Into::into)
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
let url: Url = url.into();
let canon = Providers::new(url).await?.canonicalize(url.loc).await?;
Ok(match url.scheme {
SchemeRef::Regular | SchemeRef::Search(_) => canon.into(),
SchemeRef::Archive(_) => {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))?
}
SchemeRef::Sftp(_) => UrlBuf { loc: canon.into(), scheme: url.scheme.into() },
})
}
#[inline]
pub async fn casefold<'a, U>(url: U) -> io::Result<UrlBuf>
where
U: Into<Url<'a>>,
@ -49,69 +74,70 @@ where
}
}
#[inline]
pub async fn copy<'a, U, V>(from: U, to: V, cha: Cha) -> io::Result<u64>
where
U: Into<Url<'a>>,
V: Into<Url<'a>>,
{
if let (Some(from), Some(to)) = (from.into().as_path(), to.into().as_path()) {
Local.copy(from, to, cha).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
let (from, to): (Url, Url) = (from.into(), to.into());
match (from.as_path(), to.as_path()) {
(Some(from), Some(to)) => Local.copy(from, to, cha).await,
(None, None) if from.scheme.covariant(to.scheme) => {
Providers::new(from).await?.copy(from.loc, to.loc, cha).await
}
(Some(_), None) | (None, Some(_)) | (None, None) => {
let src = Providers::new(from).await?.open(from.loc).await?;
let dist = Providers::new(to).await?.create(to.loc).await?;
let mut reader = BufReader::with_capacity(524288, src);
let mut writer = BufWriter::with_capacity(524288, dist);
let written = tokio::io::copy(&mut reader, &mut writer).await?;
writer.into_inner().set_cha(cha).await.ok();
Ok(written)
}
}
}
#[inline]
pub async fn create<'a, U>(url: U) -> io::Result<RwFile>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local.create(path).await.map(Into::into)
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
let url: Url = url.into();
Providers::new(url).await?.create(url.loc).await
}
#[inline]
pub async fn create_dir<'a, U>(url: U) -> io::Result<()>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local.create_dir(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
let url: Url = url.into();
Providers::new(url).await?.create_dir(url.loc).await
}
#[inline]
pub async fn create_dir_all<'a, U>(url: U) -> io::Result<()>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local.create_dir_all(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
let url: Url = url.into();
Providers::new(url).await?.create_dir_all(url.loc).await
}
#[inline]
pub async fn hard_link<'a, U, V>(original: U, link: V) -> io::Result<()>
where
U: Into<Url<'a>>,
V: Into<Url<'a>>,
{
if let (Some(original), Some(link)) = (original.into().as_path(), link.into().as_path()) {
Local.hard_link(original, link).await
let (original, link): (Url, Url) = (original.into(), link.into());
if original.scheme.covariant(link.scheme) {
Providers::new(original).await?.hard_link(original.loc, link.loc).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
Err(io::Error::new(io::ErrorKind::CrossesDevices, "Cross-filesystem hardlink"))
}
}
#[inline]
pub async fn identical<'a, U, V>(a: U, b: V) -> io::Result<bool>
where
U: Into<Url<'a>>,
@ -124,19 +150,14 @@ where
}
}
#[inline]
pub async fn metadata<'a, U>(url: U) -> io::Result<Cha>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local.metadata(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
let url: Url = url.into();
Providers::new(url).await?.metadata(url.loc).await
}
#[inline]
pub async fn must_identical<'a, U, V>(a: U, b: V) -> bool
where
U: Into<Url<'a>>,
@ -145,155 +166,105 @@ where
identical(a, b).await.unwrap_or(false)
}
#[inline]
pub async fn read_dir<'a, U>(url: U) -> io::Result<ReadDir>
where
U: Into<Url<'a>>,
{
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!(),
})
Providers::new(url).await?.read_dir(url.loc).await
}
#[inline]
pub async fn read_link<'a, U>(url: U) -> io::Result<PathBuf>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local.read_link(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
let url: Url = url.into();
Providers::new(url).await?.read_link(url.loc).await
}
#[inline]
pub async fn remove_dir<'a, U>(url: U) -> io::Result<()>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local.remove_dir(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
let url: Url = url.into();
Providers::new(url).await?.remove_dir(url.loc).await
}
#[inline]
pub async fn remove_dir_all<'a, U>(url: U) -> io::Result<()>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local.remove_dir_all(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
let url: Url = url.into();
Providers::new(url).await?.remove_dir_all(url.loc).await
}
#[inline]
pub async fn remove_file<'a, U>(url: U) -> io::Result<()>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local.remove_file(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
let url: Url = url.into();
Providers::new(url).await?.remove_file(url.loc).await
}
#[inline]
pub async fn rename<'a, U, V>(from: U, to: V) -> io::Result<()>
where
U: Into<Url<'a>>,
V: Into<Url<'a>>,
{
if let (Some(from), Some(to)) = (from.into().as_path(), to.into().as_path()) {
Local.rename(from, to).await
let (from, to): (Url, Url) = (from.into(), to.into());
if from.scheme.covariant(to.scheme) {
Providers::new(from).await?.rename(from.loc, to.loc).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
Err(io::Error::new(io::ErrorKind::CrossesDevices, "Cross-filesystem rename"))
}
}
#[inline]
pub async fn symlink<'a, U, F>(original: &Path, link: U, is_dir: F) -> io::Result<()>
where
U: Into<Url<'a>>,
F: AsyncFnOnce() -> io::Result<bool>,
{
if let Some(link) = link.into().as_path() {
Local.symlink(original, link, is_dir).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
let link: Url = link.into();
Providers::new(link).await?.symlink(original, link.loc, is_dir).await
}
#[inline]
pub async fn symlink_dir<'a, U>(original: &Path, link: U) -> io::Result<()>
where
U: Into<Url<'a>>,
{
if let Some(link) = link.into().as_path() {
Local.symlink_dir(original, link).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
let link: Url = link.into();
Providers::new(link).await?.symlink_dir(original, link.loc).await
}
#[inline]
pub async fn symlink_file<'a, U>(original: &Path, link: U) -> io::Result<()>
where
U: Into<Url<'a>>,
{
if let Some(link) = link.into().as_path() {
Local.symlink_file(original, link).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
let link: Url = link.into();
Providers::new(link).await?.symlink_file(original, link.loc).await
}
#[inline]
pub async fn symlink_metadata<'a, U>(url: U) -> io::Result<Cha>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local.symlink_metadata(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
let url: Url = url.into();
Providers::new(url).await?.symlink_metadata(url.loc).await
}
#[inline]
pub async fn trash<'a, U>(url: U) -> io::Result<()>
where
U: Into<Url<'a>>,
{
if let Some(path) = url.into().as_path() {
Local.trash(path).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
let url: Url = url.into();
Providers::new(url).await?.trash(url.loc).await
}
#[inline]
pub async fn write<'a, U, C>(url: U, contents: C) -> io::Result<()>
where
U: Into<Url<'a>>,
C: AsRef<[u8]>,
{
if let Some(path) = url.into().as_path() {
Local.write(path, contents).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
let url: Url = url.into();
Providers::new(url).await?.write(url.loc, contents).await
}

View file

@ -1,6 +1,6 @@
use std::{io, path::{Path, PathBuf}, sync::Arc};
use yazi_shared::{scheme::SchemeRef, url::Url};
use yazi_shared::{scheme::SchemeRef, url::{Url, UrlCow}};
use yazi_vfs::config::{ProviderSftp, Vfs};
use super::local::Local;
@ -34,13 +34,13 @@ impl Provider for Providers<'_> {
type Gate = super::Gate;
type ReadDir = super::ReadDir;
fn cache<P>(&self, path: P) -> Option<PathBuf>
async fn absolute<'a, U>(&self, url: U) -> io::Result<UrlCow<'a>>
where
P: AsRef<Path>,
U: Into<Url<'a>>,
{
match self.0 {
Inner::Regular | Inner::Search(_) => Local.cache(path),
Inner::Sftp((p, _)) => p.cache(path),
Inner::Regular | Inner::Search(_) => Local.absolute(url).await,
Inner::Sftp((p, _)) => p.absolute(url).await,
}
}

View file

@ -1,7 +1,9 @@
use std::pin::Pin;
use std::{io, pin::Pin};
use tokio::io::{AsyncRead, AsyncWrite};
use crate::cha::Cha;
pub enum RwFile {
Tokio(tokio::fs::File),
Sftp(Box<yazi_sftp::fs::File>),
@ -15,13 +17,34 @@ impl From<yazi_sftp::fs::File> for RwFile {
fn from(f: yazi_sftp::fs::File) -> Self { Self::Sftp(Box::new(f)) }
}
impl RwFile {
pub async fn set_cha(&self, cha: Cha) -> io::Result<()> {
match self {
Self::Tokio(f) => {
let std = f.try_clone().await?.into_std().await;
tokio::task::spawn_blocking(move || {
#[cfg(unix)]
std.set_permissions(cha.into()).ok();
std.set_times(cha.into()).ok();
})
.await?;
}
Self::Sftp(f) => {
f.fsetstat(&cha.into()).await?;
}
}
Ok(())
}
}
impl AsyncRead for RwFile {
#[inline]
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
) -> std::task::Poll<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),
@ -35,7 +58,7 @@ impl AsyncWrite for RwFile {
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<Result<usize, std::io::Error>> {
) -> std::task::Poll<Result<usize, 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),
@ -46,7 +69,7 @@ impl AsyncWrite for RwFile {
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), std::io::Error>> {
) -> std::task::Poll<Result<(), io::Error>> {
match &mut *self {
Self::Tokio(f) => Pin::new(f).poll_flush(cx),
Self::Sftp(f) => Pin::new(f).poll_flush(cx),
@ -57,7 +80,7 @@ impl AsyncWrite for RwFile {
fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), std::io::Error>> {
) -> std::task::Poll<Result<(), io::Error>> {
match &mut *self {
Self::Tokio(f) => Pin::new(f).poll_shutdown(cx),
Self::Sftp(f) => Pin::new(f).poll_shutdown(cx),
@ -68,8 +91,8 @@ impl AsyncWrite for RwFile {
fn poll_write_vectored(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
bufs: &[std::io::IoSlice<'_>],
) -> std::task::Poll<Result<usize, std::io::Error>> {
bufs: &[io::IoSlice<'_>],
) -> std::task::Poll<Result<usize, 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),

View file

@ -4,12 +4,13 @@ use yazi_sftp::fs::{Attrs, Flags};
use yazi_shared::scheme::SchemeRef;
use yazi_vfs::config::{ProviderSftp, Vfs};
use crate::provider::FileBuilder;
use crate::{cha::Cha, provider::FileBuilder};
pub struct Gate {
sftp: super::Sftp,
append: bool,
cha: Option<Cha>,
create: bool,
create_new: bool,
read: bool,
@ -25,6 +26,11 @@ impl FileBuilder for Gate {
self
}
fn cha(&mut self, cha: Cha) -> &mut Self {
self.cha = Some(cha);
self
}
fn create(&mut self, create: bool) -> &mut Self {
self.create = create;
self
@ -45,6 +51,7 @@ impl FileBuilder for Gate {
sftp,
append: false,
cha: None,
create: false,
create_new: false,
read: false,
@ -77,7 +84,9 @@ impl FileBuilder for Gate {
flags |= Flags::WRITE;
}
Ok(self.sftp.op().await?.open(&path, flags, Attrs::default()).await?)
let attrs = self.cha.map_or(Attrs::default(), Attrs::from);
Ok(self.sftp.op().await?.open(&path, flags, &attrs).await?)
}
fn read(&mut self, read: bool) -> &mut Self {

View file

@ -1,10 +1,12 @@
use std::{io, path::{Path, PathBuf}, sync::Arc};
use russh::keys::PrivateKeyWithHashAlg;
use tokio::io::{BufReader, BufWriter};
use yazi_sftp::fs::{Attrs, Flags};
use yazi_shared::scheme::SchemeRef;
use yazi_shared::{scheme::SchemeRef, url::{Url, UrlBuf, UrlCow}};
use yazi_vfs::config::ProviderSftp;
use crate::{cha::Cha, provider::{FileBuilder, Provider}};
use crate::{cha::Cha, provider::{FileBuilder, Provider, local::Local}};
#[derive(Clone, Copy)]
pub struct Sftp {
@ -21,11 +23,18 @@ impl Provider for Sftp {
type Gate = super::Gate;
type ReadDir = super::ReadDir;
fn cache<P>(&self, _: P) -> Option<PathBuf>
async fn absolute<'a, U>(&self, url: U) -> io::Result<UrlCow<'a>>
where
P: AsRef<Path>,
U: Into<Url<'a>>,
{
todo!()
let url: Url = url.into();
Ok(if url.is_absolute() {
url.into()
} else if let SchemeRef::Sftp(_) = url.scheme {
UrlBuf { loc: self.canonicalize(url.loc).await?.into(), scheme: url.scheme.into() }.into()
} else {
Err(io::Error::new(io::ErrorKind::InvalidInput, "Not an SFTP URL"))?
})
}
async fn canonicalize<P>(&self, path: P) -> io::Result<PathBuf>
@ -43,10 +52,16 @@ impl Provider for Sftp {
let attrs = Attrs::from(cha);
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?;
let from = op.open(&from, Flags::READ, &Attrs::default()).await?;
let to = op.open(&to, Flags::WRITE | Flags::CREATE | Flags::TRUNCATE, &attrs).await?;
tokio::io::copy(&mut from, &mut to).await
let mut reader = BufReader::with_capacity(524288, from);
let mut writer = BufWriter::with_capacity(524288, to);
let written = tokio::io::copy(&mut reader, &mut writer).await?;
writer.into_inner().fsetstat(&attrs).await.ok();
Ok(written)
}
async fn create_dir<P>(&self, path: P) -> io::Result<()>
@ -144,7 +159,7 @@ impl Sftp {
use deadpool::managed::PoolError;
let pool = *super::CONN.lock().entry(self.config).or_insert_with(|| {
Box::leak(Box::new(deadpool::managed::Pool::builder(self).build().unwrap()))
Box::leak(Box::new(deadpool::managed::Pool::builder(self).max_size(5).build().unwrap()))
});
pool.get().await.map_err(|e| match e {
@ -172,45 +187,134 @@ impl deadpool::managed::Manager for Sftp {
type Error = io::Error;
type Type = yazi_sftp::Operator;
// 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 channel = self.connect().await.map_err(|e| {
io::Error::other(format!("Failed to connect to SFTP server `{}`: {e}", self.name))
})?;
// 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()))
let mut op = yazi_sftp::Operator::make(channel.into_stream());
op.init().await?;
Ok(op)
}
async fn recycle(
&self,
obj: &mut Self::Type,
metrics: &deadpool::managed::Metrics,
_metrics: &deadpool::managed::Metrics,
) -> deadpool::managed::RecycleResult<Self::Error> {
// FIXME
Ok(())
if obj.is_closed() {
Err(deadpool::managed::RecycleError::Message("Channel closed".into()))
} else {
Ok(())
}
}
}
impl Sftp {
async fn connect(self) -> Result<russh::Channel<russh::client::Msg>, russh::Error> {
let pref = Arc::new(russh::client::Config {
inactivity_timeout: Some(std::time::Duration::from_secs(30)),
keepalive_interval: Some(std::time::Duration::from_secs(10)),
nodelay: true,
..Default::default()
});
let session = if self.config.password.is_some() {
self.connect_by_password(pref).await
} else if self.config.key_file.is_some() {
self.connect_by_key(pref).await
} else {
self.connect_by_agent(pref).await
}?;
let channel = session.channel_open_session().await?;
channel.request_subsystem(true, "sftp").await?;
Ok(channel)
}
async fn connect_by_password(
self,
pref: Arc<russh::client::Config>,
) -> Result<russh::client::Handle<Self>, russh::Error> {
let Some(password) = &self.config.password else {
return Err(russh::Error::InvalidConfig("Password not provided".to_owned()));
};
let mut session =
russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?;
if session.authenticate_password(&self.config.user, password).await?.success() {
Ok(session)
} else {
Err(russh::Error::InvalidConfig("Password authentication failed".to_owned()))
}
}
async fn connect_by_key(
self,
pref: Arc<russh::client::Config>,
) -> Result<russh::client::Handle<Self>, russh::Error> {
let Some(key_file) = &self.config.key_file else {
return Err(russh::Error::InvalidConfig("Key file not provided".to_owned()));
};
let key = Local
.read_to_string(key_file)
.await
.map_err(|e| russh::Error::InvalidConfig(format!("Failed to read key file: {e}")))?;
let key = russh::keys::decode_secret_key(&key, self.config.key_passphrase.as_deref())?;
let mut session =
russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?;
let result = session
.authenticate_publickey(
&self.config.user,
PrivateKeyWithHashAlg::new(
Arc::new(key),
session.best_supported_rsa_hash().await?.flatten(),
),
)
.await?;
if result.success() {
Ok(session)
} else {
Err(russh::Error::InvalidConfig("Public key authentication failed".to_owned()))
}
}
async fn connect_by_agent(
self,
pref: Arc<russh::client::Config>,
) -> Result<russh::client::Handle<Self>, russh::Error> {
let Some(identity_agent) = &self.config.identity_agent else {
return Err(russh::Error::InvalidConfig("Identity agent not provided".to_owned()));
};
#[cfg(unix)]
let mut agent = russh::keys::agent::client::AgentClient::connect_uds(identity_agent).await?;
#[cfg(windows)]
let mut agent =
russh::keys::agent::client::AgentClient::connect_named_pipe(identity_agent).await?;
let keys = agent.request_identities().await?;
if keys.is_empty() {
return Err(russh::Error::InvalidConfig("No keys found in SSH agent".to_owned()));
}
let mut session =
russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?;
for key in keys {
match session.authenticate_publickey_with(&self.config.user, key, None, &mut agent).await {
Ok(result) if result.success() => return Ok(session),
Ok(_) => {}
Err(e) => tracing::error!("Identity agent authentication error: {e}"),
}
}
Err(russh::Error::InvalidConfig("Public key authentication via agent failed".to_owned()))
}
}

View file

@ -2,7 +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 yazi_shared::{scheme::SchemeRef, url::{Url, UrlCow}};
use crate::cha::{Cha, ChaType};
@ -11,9 +11,9 @@ pub trait Provider {
type Gate: FileBuilder<File = Self::File>;
type ReadDir: DirReader;
fn cache<P>(&self, path: P) -> Option<PathBuf>
fn absolute<'a, U>(&self, url: U) -> impl Future<Output = io::Result<UrlCow<'a>>>
where
P: AsRef<Path>;
U: Into<Url<'a>>;
fn canonicalize<P>(&self, path: P) -> impl Future<Output = io::Result<PathBuf>>
where
@ -40,26 +40,37 @@ pub trait Provider {
P: AsRef<Path>,
{
async move {
let path = path.as_ref();
let mut path = path.as_ref();
if path == Path::new("") {
return Ok(());
}
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(e) => return Err(e),
let mut stack = Vec::new();
loop {
match self.create_dir(path).await {
Ok(()) => break,
Err(e) if e.kind() == io::ErrorKind::NotFound => {
if let Some(parent) = path.parent() {
stack.push(path);
path = parent;
} else {
return Err(io::Error::other("failed to create whole tree"));
}
}
Err(_) if self.metadata(path).await.is_ok_and(|m| m.is_dir()) => break,
Err(e) => return Err(e),
}
}
match path.parent() {
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 {
Ok(()) => Ok(()),
Err(_) if self.metadata(path).await.is_ok_and(|m| m.is_dir()) => Ok(()),
Err(e) => Err(e),
while let Some(p) = stack.pop() {
match self.create_dir(p).await {
Ok(()) => {}
Err(_) if self.metadata(p).await.is_ok_and(|m| m.is_dir()) => {}
Err(e) => return Err(e),
}
}
Ok(())
}
}
@ -105,7 +116,7 @@ pub trait Provider {
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(me, &child.path()).await
Box::pin(remove_dir_all_impl(me, &child.path())).await
} else {
me.remove_file(&child.path()).await
};
@ -206,6 +217,8 @@ pub trait FileBuilder {
fn append(&mut self, append: bool) -> &mut Self;
fn cha(&mut self, cha: Cha) -> &mut Self;
fn create(&mut self, create: bool) -> &mut Self;
fn create_new(&mut self, create_new: bool) -> &mut Self;

View file

@ -2,7 +2,7 @@ use std::{collections::HashMap, fmt};
use serde::{Deserialize, Deserializer, Serialize, de::Visitor, ser::SerializeStruct};
#[derive(Debug, Default)]
#[derive(Clone, Debug, Default)]
pub struct Attrs {
pub size: Option<u64>,
pub uid: Option<u32>,

View file

@ -45,7 +45,7 @@ impl File {
Operator::from(&self.session).fstat(&self.handle).await
}
pub async fn fsetstat(&self, attrs: Attrs) -> Result<(), Error> {
pub async fn fsetstat(&self, attrs: &Attrs) -> Result<(), Error> {
Operator::from(&self.session).fsetstat(&self.handle, attrs).await
}
}

View file

@ -26,7 +26,7 @@ impl Operator {
Ok(())
}
pub async fn open<'a, P>(&self, path: P, flags: Flags, attrs: Attrs) -> Result<File, Error>
pub async fn open<'a, P>(&self, path: P, flags: Flags, attrs: &'a Attrs) -> Result<File, Error>
where
P: Into<ByteStr<'a>>,
{
@ -78,7 +78,7 @@ impl Operator {
status.into()
}
pub async fn fsetstat(&self, handle: &str, attrs: Attrs) -> Result<(), Error> {
pub async fn fsetstat<'a>(&self, handle: &str, attrs: &'a Attrs) -> Result<(), Error> {
let status: responses::Status = self.send(requests::FSetStat::new(handle, attrs)).await?;
status.into()
}

View file

@ -259,6 +259,7 @@ pub fn from_bytes(mut bytes: &[u8]) -> Result<Packet<'static>, Error> {
102 => Packet::Handle(Deserializer::once(bytes)?),
103 => Packet::Data(Deserializer::once(bytes)?),
104 => Packet::Name(Deserializer::once(bytes)?),
105 => Packet::Attrs(Deserializer::once(bytes)?),
201 => Packet::ExtendedReply(Deserializer::once(bytes)?),
_ => return Err(Error::Packet("unknown packet kind")),
})

View file

@ -1,3 +1,5 @@
use std::borrow::Cow;
use serde::{Deserialize, Serialize};
use crate::{ByteStr, fs::{Attrs, Flags}};
@ -7,15 +9,15 @@ pub struct Open<'a> {
pub id: u32,
pub path: ByteStr<'a>,
pub flags: Flags,
pub attrs: Attrs,
pub attrs: Cow<'a, Attrs>,
}
impl<'a> Open<'a> {
pub fn new<P>(path: P, flags: Flags, attrs: Attrs) -> Self
pub fn new<P>(path: P, flags: Flags, attrs: &'a Attrs) -> Self
where
P: Into<ByteStr<'a>>,
{
Self { id: 0, path: path.into(), flags, attrs }
Self { id: 0, path: path.into(), flags, attrs: Cow::Borrowed(attrs) }
}
pub fn len(&self) -> usize {

View file

@ -23,12 +23,12 @@ impl<'a> SetStat<'a> {
pub struct FSetStat<'a> {
pub id: u32,
pub handle: Cow<'a, str>,
pub attrs: Attrs,
pub attrs: Cow<'a, Attrs>,
}
impl<'a> FSetStat<'a> {
pub fn new(handle: impl Into<Cow<'a, str>>, attrs: Attrs) -> Self {
Self { id: 0, handle: handle.into(), attrs }
pub fn new(handle: impl Into<Cow<'a, str>>, attrs: &'a Attrs) -> Self {
Self { id: 0, handle: handle.into(), attrs: Cow::Borrowed(attrs) }
}
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.handle.len() + self.attrs.len() }

View file

@ -1,9 +1,9 @@
use std::{any::TypeId, collections::HashMap, io::ErrorKind, sync::Arc};
use std::{any::TypeId, collections::HashMap, io::{self, ErrorKind}, sync::Arc};
use parking_lot::Mutex;
use russh::{ChannelStream, client::Msg};
use serde::Serialize;
use tokio::{io::{AsyncReadExt, AsyncWriteExt}, select, sync::{mpsc, oneshot}};
use tokio::{io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf}, select, sync::{mpsc, oneshot}};
use crate::{Error, Id, Packet, responses};
@ -14,6 +14,10 @@ pub struct Session {
pub(super) extensions: Mutex<HashMap<String, String>>,
}
impl Drop for Session {
fn drop(&mut self) { self.tx.send(vec![]).ok(); }
}
impl Session {
pub(super) fn make(stream: ChannelStream<Msg>) -> Arc<Self> {
let (tx, mut rx) = mpsc::unbounded_channel();
@ -24,15 +28,30 @@ impl Session {
extensions: Default::default(),
});
async fn read(reader: &mut ReadHalf<ChannelStream<Msg>>) -> io::Result<Vec<u8>> {
let len = reader.read_u32().await?;
let mut buf = vec![0; len as usize];
reader.read_exact(&mut buf).await?;
Ok(buf)
}
async fn write(writer: &mut WriteHalf<ChannelStream<Msg>>, buf: Vec<u8>) -> io::Result<()> {
if buf.is_empty() {
Err(io::Error::new(ErrorKind::BrokenPipe, "channel closed"))
} else {
writer.write_all(&buf).await
}
}
let (mut reader, mut writer) = tokio::io::split(stream);
tokio::spawn(async move {
while let Some(data) = rx.recv().await {
if data.is_empty() {
if let Err(e) = write(&mut writer, data).await
&& e.kind() == ErrorKind::BrokenPipe
{
rx.close();
writer.shutdown().await.ok();
break;
} else {
writer.write_all(&data).await.ok();
}
}
});
@ -41,12 +60,16 @@ impl Session {
tokio::spawn(async move {
loop {
select! {
Ok(len) = reader.read_u32() => {
let mut buf = vec![0; len as usize];
if let Err(e) = reader.read_exact(&mut buf).await && e.kind() == ErrorKind::UnexpectedEof {
me_.tx.send(vec![]).ok();
break;
}
result = read(&mut reader) => {
let buf = match result {
Ok(b) => b,
Err(e) if e.kind() == ErrorKind::UnexpectedEof => {
me_.tx.send(vec![]).ok();
break;
},
Err(_) => continue,
};
if let Ok(packet) = crate::from_bytes(&buf)
&& let Some(cb) = me_.callback.lock().remove(&packet.id())
{
@ -89,4 +112,6 @@ impl Session {
Ok(rx)
}
pub fn is_closed(&self) -> bool { self.tx.is_closed() }
}

View file

@ -53,11 +53,11 @@ impl From<String> for LogLevel {
impl AsRef<str> for LogLevel {
fn as_ref(&self) -> &str {
match self {
Self::None => "NONE",
Self::Error => "ERROR",
Self::Warn => "WARN",
Self::Info => "INFO",
Self::Debug => "DEBUG",
Self::None => "yazi=NONE",
Self::Error => "yazi=ERROR",
Self::Warn => "yazi=WARN",
Self::Info => "yazi=INFO",
Self::Debug => "yazi=DEBUG",
}
}
}

View file

@ -205,5 +205,5 @@ impl<'a> Loc<'a> {
}
#[inline]
fn bytes(self) -> &'a [u8] { self.inner.as_os_str().as_encoded_bytes() }
pub fn bytes(self) -> &'a [u8] { self.inner.as_os_str().as_encoded_bytes() }
}

View file

@ -22,7 +22,7 @@ impl<'a> Encode<'a> {
pub(super) fn new(loc: Loc<'a>, scheme: SchemeRef<'a>) -> Self { Self { loc, scheme } }
#[inline]
fn domain<'s>(s: &'s str) -> PercentEncode<'s> {
pub fn domain<'s>(s: &'s str) -> PercentEncode<'s> {
const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':');
percent_encode(s.as_bytes(), SET)
}

View file

@ -13,7 +13,6 @@ 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 }

View file

@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::{io, path::PathBuf};
use serde::{Deserialize, Serialize};
@ -18,12 +18,33 @@ impl TryFrom<&'static Provider> for &'static ProviderSftp {
}
}
impl Provider {
pub(super) fn reshape(self) -> io::Result<Self> {
match self {
Self::Sftp(p) => p.reshape().map(Self::Sftp),
}
}
}
// --- 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>,
pub host: String,
pub user: String,
pub port: u16,
pub password: Option<String>,
pub key_file: Option<PathBuf>,
pub key_passphrase: Option<String>,
// FIXME: set default: $SSH_AUTH_SOCK
pub identity_agent: Option<PathBuf>,
}
impl ProviderSftp {
fn reshape(self) -> io::Result<Self> {
// FIXME: expand the path
// if let Some(key_file) = self.key_file {}
// if let Some(identity_agent) = self.identity_agent {}
Ok(self)
}
}

View file

@ -40,14 +40,14 @@ impl Vfs {
}
}
pub(crate) fn read() -> io::Result<String> {
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> {
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")))?;