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

View file

@ -21,10 +21,10 @@ lto = false
[workspace.dependencies] [workspace.dependencies]
ansi-to-tui = "7.0.0" ansi-to-tui = "7.0.0"
anyhow = "1.0.99" anyhow = "1.0.100"
base64 = "0.22.1" base64 = "0.22.1"
bitflags = { version = "2.9.4", features = [ "serde" ] } 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" core-foundation-sys = "0.8.7"
crossterm = { version = "0.29.0", features = [ "event-stream" ] } crossterm = { version = "0.29.0", features = [ "event-stream" ] }
dirs = "6.0.0" dirs = "6.0.0"
@ -32,8 +32,8 @@ foldhash = "0.2.0"
futures = "0.3.31" futures = "0.3.31"
globset = "0.4.16" globset = "0.4.16"
hashbrown = { version = "0.16.0", features = [ "serde" ] } hashbrown = { version = "0.16.0", features = [ "serde" ] }
indexmap = { version = "2.11.3", features = [ "serde" ] } indexmap = { version = "2.11.4", features = [ "serde" ] }
libc = "0.2.175" libc = "0.2.176"
lru = "0.16.1" lru = "0.16.1"
mlua = { version = "0.11.3", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] } mlua = { version = "0.11.3", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] }
objc = "0.2.7" objc = "0.2.7"
@ -44,13 +44,13 @@ ratatui = { version = "0.29.0", features = [ "unstable-rendered-line
regex = "1.11.2" regex = "1.11.2"
russh = { version = "0.54.3", default-features = false, features = [ "ring", "rsa" ] } russh = { version = "0.54.3", default-features = false, features = [ "ring", "rsa" ] }
scopeguard = "1.2.0" scopeguard = "1.2.0"
serde = { version = "1.0.225", features = [ "derive" ] } serde = { version = "1.0.226", features = [ "derive" ] }
serde_json = "1.0.145" serde_json = "1.0.145"
syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
tokio = { version = "1.47.1", features = [ "full" ] } tokio = { version = "1.47.1", features = [ "full" ] }
tokio-stream = "0.1.17" tokio-stream = "0.1.17"
tokio-util = "0.7.16" 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" ] } tracing = { version = "0.1.41", features = [ "max_level_debug", "release_max_level_debug" ] }
unicode-width = { version = "0.2.0", default-features = false } unicode-width = { version = "0.2.0", default-features = false }
twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] } 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 hashbrown::HashSet;
use serde::Serialize; use serde::Serialize;
use yazi_fs::{CWD, path::expand_url, provider}; 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; use yazi_vfs::local::Xdg;
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Serialize)]
@ -27,7 +27,12 @@ impl Boot {
return (vec![CWD.load().as_ref().clone()], vec![UrnBuf::default()]); 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 { let Some((parent, child)) = entry.pair() else {
return (entry, UrnBuf::default()); 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 } ratatui = { workspace = true }
regex = { workspace = true } regex = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
tokio = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }

View file

@ -31,6 +31,7 @@ scopeguard = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
twox-hash = { workspace = true }
[target."cfg(unix)".dependencies] [target."cfg(unix)".dependencies]
libc = { workspace = true } 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 arc_swap::ArcSwap;
use yazi_shared::{RoCell, url::UrlBuf}; use yazi_shared::{RoCell, url::UrlBuf};
use crate::provider;
pub static CWD: RoCell<Cwd> = RoCell::new(); pub static CWD: RoCell<Cwd> = RoCell::new();
pub struct Cwd(ArcSwap<UrlBuf>); pub struct Cwd(ArcSwap<UrlBuf>);
@ -26,16 +28,20 @@ impl Default for Cwd {
} }
impl 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 { 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; return false;
} }
self.store(Arc::new(url.clone())); self.0.store(Arc::new(url.clone()));
if let Some(p) = url.as_path() { Self::sync_cwd();
unsafe { std::env::set_var("PWD", p) };
Self::sync_cwd();
}
true true
} }
@ -47,17 +53,20 @@ impl Cwd {
} }
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
if let Some(p) = CWD.load().as_path() { let path = CWD.path();
_ = set_current_dir(p); std::fs::create_dir_all(&path).ok();
}
_ = set_current_dir(&path);
let cur = current_dir().unwrap_or_default(); let cur = current_dir().unwrap_or_default();
unsafe { std::env::set_var("PWD", path) }
SYNCING.store(false, Ordering::Relaxed); SYNCING.store(false, Ordering::Relaxed);
if let Some(p) = CWD.load().as_path() let path = CWD.path();
&& cur != p if cur != path {
{ std::fs::create_dir_all(&path).ok();
set_current_dir(p).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> { pub fn absolute_url<'a>(url: Url<'a>) -> UrlCow<'a> {
let b = url.loc.as_os_str().as_encoded_bytes(); if url.scheme.is_virtual() {
let local = !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( let loc = LocBuf::with(
format!(r"{}:\", b[0].to_ascii_uppercase() as char).into(), format!(r"{}:\", b[0].to_ascii_uppercase() as char).into(),
if url.has_base() { 0 } else { 2 }, 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"); .expect("Failed to create Loc from drive letter");
UrlBuf { loc, scheme: url.scheme.into() }.into() UrlBuf { loc, scheme: url.scheme.into() }.into()
} else if local } else if let Ok(rest) = url.loc.strip_prefix("~/")
&& let Ok(rest) = url.loc.strip_prefix("~/")
&& let Some(home) = dirs::home_dir() && let Some(home) = dirs::home_dir()
&& home.is_absolute() && home.is_absolute()
{ {
@ -88,10 +89,10 @@ fn absolute_url<'a>(url: Url<'a>) -> UrlCow<'a> {
.expect("Failed to create Loc from home directory"); .expect("Failed to create Loc from home directory");
UrlBuf { loc, scheme: url.scheme.into() }.into() UrlBuf { loc, scheme: url.scheme.into() }.into()
} else if !url.is_absolute() { } else if !url.is_absolute() {
let cwd = CWD.load(); let cwd = CWD.path();
let loc = LocBuf::with(cwd.loc.join(url.loc), url.uri().count(), url.urn().count()) let loc = LocBuf::with(cwd.join(url.loc), url.uri().count(), url.urn().count())
.expect("Failed to create Loc from relative path"); .expect("Failed to create Loc from relative path");
UrlBuf { loc, scheme: cwd.scheme.clone() }.into() UrlBuf { loc, scheme: url.scheme.into() }.into()
} else { } else {
url.into() url.into()
} }

View file

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

View file

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

View file

@ -1,6 +1,8 @@
use std::{io, path::{Path, PathBuf}}; 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)] #[derive(Clone, Copy)]
pub struct Local; pub struct Local;
@ -10,12 +12,16 @@ impl Provider for Local {
type Gate = super::Gate; type Gate = super::Gate;
type ReadDir = super::ReadDir; type ReadDir = super::ReadDir;
#[inline] async fn absolute<'a, U>(&self, url: U) -> io::Result<UrlCow<'a>>
fn cache<P>(&self, _: P) -> Option<PathBuf>
where 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] #[inline]
@ -220,23 +226,9 @@ impl Provider for Local {
impl Local { impl Local {
async fn copy_impl(from: PathBuf, to: PathBuf, cha: Cha) -> io::Result<u64> { 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"))] #[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 || { tokio::task::spawn_blocking(move || {
let mut reader = std::fs::File::open(from)?; let mut reader = std::fs::File::open(from)?;
@ -248,8 +240,8 @@ impl Local {
.open(to)?; .open(to)?;
let written = std::io::copy(&mut reader, &mut writer)?; let written = std::io::copy(&mut reader, &mut writer)?;
unsafe { libc::fchmod(writer.as_raw_fd(), cha.mode.bits() as _) }; writer.set_permissions(cha.into()).ok();
writer.set_times(ft).ok(); writer.set_times(cha.into()).ok();
Ok(written) Ok(written)
}) })
@ -260,7 +252,11 @@ impl Local {
{ {
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
let written = std::fs::copy(from, &to)?; 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) Ok(written)
}) })
.await? .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> pub fn cache<'a, U>(url: U) -> Option<PathBuf>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { Local.cache(path) } else { None } 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> pub async fn calculate<'a, U>(url: U) -> io::Result<u64>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
@ -25,19 +47,22 @@ where
} }
} }
#[inline]
pub async fn canonicalize<'a, U>(url: U) -> io::Result<UrlBuf> pub async fn canonicalize<'a, U>(url: U) -> io::Result<UrlBuf>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { let url: Url = url.into();
Local.canonicalize(path).await.map(Into::into) let canon = Providers::new(url).await?.canonicalize(url.loc).await?;
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) 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> pub async fn casefold<'a, U>(url: U) -> io::Result<UrlBuf>
where where
U: Into<Url<'a>>, 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> pub async fn copy<'a, U, V>(from: U, to: V, cha: Cha) -> io::Result<u64>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
V: Into<Url<'a>>, V: Into<Url<'a>>,
{ {
if let (Some(from), Some(to)) = (from.into().as_path(), to.into().as_path()) { let (from, to): (Url, Url) = (from.into(), to.into());
Local.copy(from, to, cha).await
} else { match (from.as_path(), to.as_path()) {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) (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> pub async fn create<'a, U>(url: U) -> io::Result<RwFile>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { let url: Url = url.into();
Local.create(path).await.map(Into::into) Providers::new(url).await?.create(url.loc).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
} }
#[inline]
pub async fn create_dir<'a, U>(url: U) -> io::Result<()> pub async fn create_dir<'a, U>(url: U) -> io::Result<()>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { let url: Url = url.into();
Local.create_dir(path).await Providers::new(url).await?.create_dir(url.loc).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
} }
#[inline]
pub async fn create_dir_all<'a, U>(url: U) -> io::Result<()> pub async fn create_dir_all<'a, U>(url: U) -> io::Result<()>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { let url: Url = url.into();
Local.create_dir_all(path).await Providers::new(url).await?.create_dir_all(url.loc).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
} }
#[inline]
pub async fn hard_link<'a, U, V>(original: U, link: V) -> io::Result<()> pub async fn hard_link<'a, U, V>(original: U, link: V) -> io::Result<()>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
V: Into<Url<'a>>, V: Into<Url<'a>>,
{ {
if let (Some(original), Some(link)) = (original.into().as_path(), link.into().as_path()) { let (original, link): (Url, Url) = (original.into(), link.into());
Local.hard_link(original, link).await if original.scheme.covariant(link.scheme) {
Providers::new(original).await?.hard_link(original.loc, link.loc).await
} else { } 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> pub async fn identical<'a, U, V>(a: U, b: V) -> io::Result<bool>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
@ -124,19 +150,14 @@ where
} }
} }
#[inline]
pub async fn metadata<'a, U>(url: U) -> io::Result<Cha> pub async fn metadata<'a, U>(url: U) -> io::Result<Cha>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { let url: Url = url.into();
Local.metadata(path).await Providers::new(url).await?.metadata(url.loc).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
} }
#[inline]
pub async fn must_identical<'a, U, V>(a: U, b: V) -> bool pub async fn must_identical<'a, U, V>(a: U, b: V) -> bool
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
@ -145,155 +166,105 @@ where
identical(a, b).await.unwrap_or(false) identical(a, b).await.unwrap_or(false)
} }
#[inline]
pub async fn read_dir<'a, U>(url: U) -> io::Result<ReadDir> pub async fn read_dir<'a, U>(url: U) -> io::Result<ReadDir>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
let url: Url = url.into(); let url: Url = url.into();
Ok(match url.scheme { Providers::new(url).await?.read_dir(url.loc).await
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]
pub async fn read_link<'a, U>(url: U) -> io::Result<PathBuf> pub async fn read_link<'a, U>(url: U) -> io::Result<PathBuf>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { let url: Url = url.into();
Local.read_link(path).await Providers::new(url).await?.read_link(url.loc).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
} }
#[inline]
pub async fn remove_dir<'a, U>(url: U) -> io::Result<()> pub async fn remove_dir<'a, U>(url: U) -> io::Result<()>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { let url: Url = url.into();
Local.remove_dir(path).await Providers::new(url).await?.remove_dir(url.loc).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
} }
#[inline]
pub async fn remove_dir_all<'a, U>(url: U) -> io::Result<()> pub async fn remove_dir_all<'a, U>(url: U) -> io::Result<()>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { let url: Url = url.into();
Local.remove_dir_all(path).await Providers::new(url).await?.remove_dir_all(url.loc).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
} }
#[inline]
pub async fn remove_file<'a, U>(url: U) -> io::Result<()> pub async fn remove_file<'a, U>(url: U) -> io::Result<()>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { let url: Url = url.into();
Local.remove_file(path).await Providers::new(url).await?.remove_file(url.loc).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
} }
#[inline]
pub async fn rename<'a, U, V>(from: U, to: V) -> io::Result<()> pub async fn rename<'a, U, V>(from: U, to: V) -> io::Result<()>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
V: Into<Url<'a>>, V: Into<Url<'a>>,
{ {
if let (Some(from), Some(to)) = (from.into().as_path(), to.into().as_path()) { let (from, to): (Url, Url) = (from.into(), to.into());
Local.rename(from, to).await if from.scheme.covariant(to.scheme) {
Providers::new(from).await?.rename(from.loc, to.loc).await
} else { } 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<()> pub async fn symlink<'a, U, F>(original: &Path, link: U, is_dir: F) -> io::Result<()>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
F: AsyncFnOnce() -> io::Result<bool>, F: AsyncFnOnce() -> io::Result<bool>,
{ {
if let Some(link) = link.into().as_path() { let link: Url = link.into();
Local.symlink(original, link, is_dir).await Providers::new(link).await?.symlink(original, link.loc, is_dir).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
} }
#[inline]
pub async fn symlink_dir<'a, U>(original: &Path, link: U) -> io::Result<()> pub async fn symlink_dir<'a, U>(original: &Path, link: U) -> io::Result<()>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(link) = link.into().as_path() { let link: Url = link.into();
Local.symlink_dir(original, link).await Providers::new(link).await?.symlink_dir(original, link.loc).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
} }
#[inline]
pub async fn symlink_file<'a, U>(original: &Path, link: U) -> io::Result<()> pub async fn symlink_file<'a, U>(original: &Path, link: U) -> io::Result<()>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(link) = link.into().as_path() { let link: Url = link.into();
Local.symlink_file(original, link).await Providers::new(link).await?.symlink_file(original, link.loc).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
} }
#[inline]
pub async fn symlink_metadata<'a, U>(url: U) -> io::Result<Cha> pub async fn symlink_metadata<'a, U>(url: U) -> io::Result<Cha>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { let url: Url = url.into();
Local.symlink_metadata(path).await Providers::new(url).await?.symlink_metadata(url.loc).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
} }
#[inline]
pub async fn trash<'a, U>(url: U) -> io::Result<()> pub async fn trash<'a, U>(url: U) -> io::Result<()>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { let url: Url = url.into();
Local.trash(path).await Providers::new(url).await?.trash(url.loc).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
} }
#[inline]
pub async fn write<'a, U, C>(url: U, contents: C) -> io::Result<()> pub async fn write<'a, U, C>(url: U, contents: C) -> io::Result<()>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
C: AsRef<[u8]>, C: AsRef<[u8]>,
{ {
if let Some(path) = url.into().as_path() { let url: Url = url.into();
Local.write(path, contents).await Providers::new(url).await?.write(url.loc, contents).await
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
}
} }

View file

@ -1,6 +1,6 @@
use std::{io, path::{Path, PathBuf}, sync::Arc}; 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 yazi_vfs::config::{ProviderSftp, Vfs};
use super::local::Local; use super::local::Local;
@ -34,13 +34,13 @@ impl Provider for Providers<'_> {
type Gate = super::Gate; type Gate = super::Gate;
type ReadDir = super::ReadDir; 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 where
P: AsRef<Path>, U: Into<Url<'a>>,
{ {
match self.0 { match self.0 {
Inner::Regular | Inner::Search(_) => Local.cache(path), Inner::Regular | Inner::Search(_) => Local.absolute(url).await,
Inner::Sftp((p, _)) => p.cache(path), 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 tokio::io::{AsyncRead, AsyncWrite};
use crate::cha::Cha;
pub enum RwFile { pub enum RwFile {
Tokio(tokio::fs::File), Tokio(tokio::fs::File),
Sftp(Box<yazi_sftp::fs::File>), Sftp(Box<yazi_sftp::fs::File>),
@ -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)) } 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 { impl AsyncRead for RwFile {
#[inline] #[inline]
fn poll_read( fn poll_read(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>, cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>, buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> { ) -> std::task::Poll<io::Result<()>> {
match &mut *self { match &mut *self {
Self::Tokio(f) => Pin::new(f).poll_read(cx, buf), Self::Tokio(f) => Pin::new(f).poll_read(cx, buf),
Self::Sftp(f) => Pin::new(f).poll_read(cx, buf), Self::Sftp(f) => Pin::new(f).poll_read(cx, buf),
@ -35,7 +58,7 @@ impl AsyncWrite for RwFile {
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>, cx: &mut std::task::Context<'_>,
buf: &[u8], buf: &[u8],
) -> std::task::Poll<Result<usize, std::io::Error>> { ) -> std::task::Poll<Result<usize, io::Error>> {
match &mut *self { match &mut *self {
Self::Tokio(f) => Pin::new(f).poll_write(cx, buf), Self::Tokio(f) => Pin::new(f).poll_write(cx, buf),
Self::Sftp(f) => Pin::new(f).poll_write(cx, buf), Self::Sftp(f) => Pin::new(f).poll_write(cx, buf),
@ -46,7 +69,7 @@ impl AsyncWrite for RwFile {
fn poll_flush( fn poll_flush(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>, cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), std::io::Error>> { ) -> std::task::Poll<Result<(), io::Error>> {
match &mut *self { match &mut *self {
Self::Tokio(f) => Pin::new(f).poll_flush(cx), Self::Tokio(f) => Pin::new(f).poll_flush(cx),
Self::Sftp(f) => Pin::new(f).poll_flush(cx), Self::Sftp(f) => Pin::new(f).poll_flush(cx),
@ -57,7 +80,7 @@ impl AsyncWrite for RwFile {
fn poll_shutdown( fn poll_shutdown(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>, cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), std::io::Error>> { ) -> std::task::Poll<Result<(), io::Error>> {
match &mut *self { match &mut *self {
Self::Tokio(f) => Pin::new(f).poll_shutdown(cx), Self::Tokio(f) => Pin::new(f).poll_shutdown(cx),
Self::Sftp(f) => Pin::new(f).poll_shutdown(cx), Self::Sftp(f) => Pin::new(f).poll_shutdown(cx),
@ -68,8 +91,8 @@ impl AsyncWrite for RwFile {
fn poll_write_vectored( fn poll_write_vectored(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>, cx: &mut std::task::Context<'_>,
bufs: &[std::io::IoSlice<'_>], bufs: &[io::IoSlice<'_>],
) -> std::task::Poll<Result<usize, std::io::Error>> { ) -> std::task::Poll<Result<usize, io::Error>> {
match &mut *self { match &mut *self {
Self::Tokio(f) => Pin::new(f).poll_write_vectored(cx, bufs), Self::Tokio(f) => Pin::new(f).poll_write_vectored(cx, bufs),
Self::Sftp(f) => Pin::new(f).poll_write_vectored(cx, bufs), Self::Sftp(f) => Pin::new(f).poll_write_vectored(cx, bufs),

View file

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

View file

@ -1,10 +1,12 @@
use std::{io, path::{Path, PathBuf}, sync::Arc}; 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_sftp::fs::{Attrs, Flags};
use yazi_shared::scheme::SchemeRef; use yazi_shared::{scheme::SchemeRef, url::{Url, UrlBuf, UrlCow}};
use yazi_vfs::config::ProviderSftp; use yazi_vfs::config::ProviderSftp;
use crate::{cha::Cha, provider::{FileBuilder, Provider}}; use crate::{cha::Cha, provider::{FileBuilder, Provider, local::Local}};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct Sftp { pub struct Sftp {
@ -21,11 +23,18 @@ impl Provider for Sftp {
type Gate = super::Gate; type Gate = super::Gate;
type ReadDir = super::ReadDir; type ReadDir = super::ReadDir;
fn cache<P>(&self, _: P) -> Option<PathBuf> async fn absolute<'a, U>(&self, url: U) -> io::Result<UrlCow<'a>>
where 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> async fn canonicalize<P>(&self, path: P) -> io::Result<PathBuf>
@ -43,10 +52,16 @@ impl Provider for Sftp {
let attrs = Attrs::from(cha); let attrs = Attrs::from(cha);
let op = self.op().await?; let op = self.op().await?;
let mut from = op.open(&from, Flags::READ, Attrs::default()).await?; let from = op.open(&from, Flags::READ, &Attrs::default()).await?;
let mut to = op.open(&to, Flags::WRITE | Flags::CREATE | Flags::TRUNCATE, attrs).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<()> async fn create_dir<P>(&self, path: P) -> io::Result<()>
@ -144,7 +159,7 @@ impl Sftp {
use deadpool::managed::PoolError; use deadpool::managed::PoolError;
let pool = *super::CONN.lock().entry(self.config).or_insert_with(|| { 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 { pool.get().await.map_err(|e| match e {
@ -172,45 +187,134 @@ impl deadpool::managed::Manager for Sftp {
type Error = io::Error; type Error = io::Error;
type Type = yazi_sftp::Operator; type Type = yazi_sftp::Operator;
// FIXME: remove the hardcoded test values
async fn create(&self) -> Result<Self::Type, Self::Error> { async fn create(&self) -> Result<Self::Type, Self::Error> {
todo!() let channel = self.connect().await.map_err(|e| {
// async fn inner(sftp: Sftp) -> anyhow::Result<yazi_sftp::Operator> { io::Error::other(format!("Failed to connect to SFTP server `{}`: {e}", self.name))
// 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( let mut op = yazi_sftp::Operator::make(channel.into_stream());
// "/Users/ika/Library/Group op.init().await?;
// Containers/2BUA8C4S2C.com.1password/t/agent.sock", ) Ok(op)
// .await?;
// let mut keys = agent.request_identities().await?;
// if !session
// .authenticate_publickey_with("root", keys.remove(0), None, &mut agent)
// .await?
// .success()
// {
// panic!("auth failed");
// }
// let channel = session.channel_open_session().await?;
// channel.request_subsystem(true, "sftp").await?;
// let mut op = yazi_sftp::Operator::make(channel.into_stream());
// op.init().await?;
// Ok(op)
// }
// inner(*self).await.map_err(|e| io::Error::other(e.to_string()))
} }
async fn recycle( async fn recycle(
&self, &self,
obj: &mut Self::Type, obj: &mut Self::Type,
metrics: &deadpool::managed::Metrics, _metrics: &deadpool::managed::Metrics,
) -> deadpool::managed::RecycleResult<Self::Error> { ) -> deadpool::managed::RecycleResult<Self::Error> {
// FIXME if obj.is_closed() {
Ok(()) 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 tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
use yazi_macro::ok_or_not_found; use yazi_macro::ok_or_not_found;
use yazi_shared::scheme::SchemeRef; use yazi_shared::{scheme::SchemeRef, url::{Url, UrlCow}};
use crate::cha::{Cha, ChaType}; use crate::cha::{Cha, ChaType};
@ -11,9 +11,9 @@ pub trait Provider {
type Gate: FileBuilder<File = Self::File>; type Gate: FileBuilder<File = Self::File>;
type ReadDir: DirReader; 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 where
P: AsRef<Path>; U: Into<Url<'a>>;
fn canonicalize<P>(&self, path: P) -> impl Future<Output = io::Result<PathBuf>> fn canonicalize<P>(&self, path: P) -> impl Future<Output = io::Result<PathBuf>>
where where
@ -40,26 +40,37 @@ pub trait Provider {
P: AsRef<Path>, P: AsRef<Path>,
{ {
async move { async move {
let path = path.as_ref(); let mut path = path.as_ref();
if path == Path::new("") { if path == Path::new("") {
return Ok(()); return Ok(());
} }
match self.create_dir(path).await { let mut stack = Vec::new();
Ok(()) => return Ok(()), loop {
Err(e) if e.kind() == io::ErrorKind::NotFound => {} match self.create_dir(path).await {
Err(_) if self.metadata(path).await.is_ok_and(|m| m.is_dir()) => return Ok(()), Ok(()) => break,
Err(e) => return Err(e), 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?, while let Some(p) = stack.pop() {
None => return Err(io::Error::other("failed to create whole tree")), match self.create_dir(p).await {
} Ok(()) => {}
match self.create_dir(path).await { Err(_) if self.metadata(p).await.is_ok_and(|m| m.is_dir()) => {}
Ok(()) => Ok(()), Err(e) => return Err(e),
Err(_) if self.metadata(path).await.is_ok_and(|m| m.is_dir()) => Ok(()), }
Err(e) => Err(e),
} }
Ok(())
} }
} }
@ -105,7 +116,7 @@ pub trait Provider {
while let Some(child) = it.next().await? { while let Some(child) = it.next().await? {
let ft = ok_or_not_found!(child.file_type().await, continue); let ft = ok_or_not_found!(child.file_type().await, continue);
let result = if ft.is_dir() { let result = if ft.is_dir() {
remove_dir_all_impl(me, &child.path()).await Box::pin(remove_dir_all_impl(me, &child.path())).await
} else { } else {
me.remove_file(&child.path()).await me.remove_file(&child.path()).await
}; };
@ -206,6 +217,8 @@ pub trait FileBuilder {
fn append(&mut self, append: bool) -> &mut Self; 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(&mut self, create: bool) -> &mut Self;
fn create_new(&mut self, create_new: 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}; use serde::{Deserialize, Deserializer, Serialize, de::Visitor, ser::SerializeStruct};
#[derive(Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct Attrs { pub struct Attrs {
pub size: Option<u64>, pub size: Option<u64>,
pub uid: Option<u32>, pub uid: Option<u32>,

View file

@ -45,7 +45,7 @@ impl File {
Operator::from(&self.session).fstat(&self.handle).await 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 Operator::from(&self.session).fsetstat(&self.handle, attrs).await
} }
} }

View file

@ -26,7 +26,7 @@ impl Operator {
Ok(()) 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 where
P: Into<ByteStr<'a>>, P: Into<ByteStr<'a>>,
{ {
@ -78,7 +78,7 @@ impl Operator {
status.into() 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?; let status: responses::Status = self.send(requests::FSetStat::new(handle, attrs)).await?;
status.into() 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)?), 102 => Packet::Handle(Deserializer::once(bytes)?),
103 => Packet::Data(Deserializer::once(bytes)?), 103 => Packet::Data(Deserializer::once(bytes)?),
104 => Packet::Name(Deserializer::once(bytes)?), 104 => Packet::Name(Deserializer::once(bytes)?),
105 => Packet::Attrs(Deserializer::once(bytes)?),
201 => Packet::ExtendedReply(Deserializer::once(bytes)?), 201 => Packet::ExtendedReply(Deserializer::once(bytes)?),
_ => return Err(Error::Packet("unknown packet kind")), _ => return Err(Error::Packet("unknown packet kind")),
}) })

View file

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

View file

@ -23,12 +23,12 @@ impl<'a> SetStat<'a> {
pub struct FSetStat<'a> { pub struct FSetStat<'a> {
pub id: u32, pub id: u32,
pub handle: Cow<'a, str>, pub handle: Cow<'a, str>,
pub attrs: Attrs, pub attrs: Cow<'a, Attrs>,
} }
impl<'a> FSetStat<'a> { impl<'a> FSetStat<'a> {
pub fn new(handle: impl Into<Cow<'a, str>>, attrs: Attrs) -> Self { pub fn new(handle: impl Into<Cow<'a, str>>, attrs: &'a Attrs) -> Self {
Self { id: 0, handle: handle.into(), attrs } 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() } 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 parking_lot::Mutex;
use russh::{ChannelStream, client::Msg}; use russh::{ChannelStream, client::Msg};
use serde::Serialize; 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}; use crate::{Error, Id, Packet, responses};
@ -14,6 +14,10 @@ pub struct Session {
pub(super) extensions: Mutex<HashMap<String, String>>, pub(super) extensions: Mutex<HashMap<String, String>>,
} }
impl Drop for Session {
fn drop(&mut self) { self.tx.send(vec![]).ok(); }
}
impl Session { impl Session {
pub(super) fn make(stream: ChannelStream<Msg>) -> Arc<Self> { pub(super) fn make(stream: ChannelStream<Msg>) -> Arc<Self> {
let (tx, mut rx) = mpsc::unbounded_channel(); let (tx, mut rx) = mpsc::unbounded_channel();
@ -24,15 +28,30 @@ impl Session {
extensions: Default::default(), 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); let (mut reader, mut writer) = tokio::io::split(stream);
tokio::spawn(async move { tokio::spawn(async move {
while let Some(data) = rx.recv().await { 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(); rx.close();
writer.shutdown().await.ok(); writer.shutdown().await.ok();
break; break;
} else {
writer.write_all(&data).await.ok();
} }
} }
}); });
@ -41,12 +60,16 @@ impl Session {
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {
select! { select! {
Ok(len) = reader.read_u32() => { result = read(&mut reader) => {
let mut buf = vec![0; len as usize]; let buf = match result {
if let Err(e) = reader.read_exact(&mut buf).await && e.kind() == ErrorKind::UnexpectedEof { Ok(b) => b,
me_.tx.send(vec![]).ok(); Err(e) if e.kind() == ErrorKind::UnexpectedEof => {
break; me_.tx.send(vec![]).ok();
} break;
},
Err(_) => continue,
};
if let Ok(packet) = crate::from_bytes(&buf) if let Ok(packet) = crate::from_bytes(&buf)
&& let Some(cb) = me_.callback.lock().remove(&packet.id()) && let Some(cb) = me_.callback.lock().remove(&packet.id())
{ {
@ -89,4 +112,6 @@ impl Session {
Ok(rx) 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 { impl AsRef<str> for LogLevel {
fn as_ref(&self) -> &str { fn as_ref(&self) -> &str {
match self { match self {
Self::None => "NONE", Self::None => "yazi=NONE",
Self::Error => "ERROR", Self::Error => "yazi=ERROR",
Self::Warn => "WARN", Self::Warn => "yazi=WARN",
Self::Info => "INFO", Self::Info => "yazi=INFO",
Self::Debug => "DEBUG", Self::Debug => "yazi=DEBUG",
} }
} }
} }

View file

@ -205,5 +205,5 @@ impl<'a> Loc<'a> {
} }
#[inline] #[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 } } pub(super) fn new(loc: Loc<'a>, scheme: SchemeRef<'a>) -> Self { Self { loc, scheme } }
#[inline] #[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':'); const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':');
percent_encode(s.as_bytes(), SET) 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" } yazi-shared = { path = "../yazi-shared", version = "25.9.15" }
# External dependencies # External dependencies
anyhow = { workspace = true }
dirs = { workspace = true } dirs = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
serde = { workspace = true } serde = { workspace = true }

View file

@ -1,4 +1,4 @@
use std::path::PathBuf; use std::{io, path::PathBuf};
use serde::{Deserialize, Serialize}; 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 // --- SFTP
#[derive(Deserialize, Hash, Serialize, Eq, PartialEq)] #[derive(Deserialize, Hash, Serialize, Eq, PartialEq)]
pub struct ProviderSftp { pub struct ProviderSftp {
pub host: String, pub host: String,
pub user: String, pub user: String,
pub port: u16, pub port: u16,
pub password: Option<String>, pub password: Option<String>,
pub key_file: Option<PathBuf>, 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"); let p = Xdg::config_dir().join("vfs.toml");
Ok(ok_or_not_found!(std::fs::read_to_string(&p).map_err(|e| { 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}")) 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() { for name in self.providers.keys() {
if name.is_empty() || name.len() > 20 { if name.is_empty() || name.len() > 20 {
Err(io::Error::other(format!("VFS name `{name}` must be between 1 and 20 characters")))?; Err(io::Error::other(format!("VFS name `{name}` must be between 1 and 20 characters")))?;