diff --git a/Cargo.lock b/Cargo.lock index 11da3500..73046b65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 0c9f6c48..297afb39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" ] } diff --git a/yazi-boot/src/boot.rs b/yazi-boot/src/boot.rs index 8cd367a9..20806294 100644 --- a/yazi-boot/src/boot.rs +++ b/yazi-boot/src/boot.rs @@ -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() } } diff --git a/yazi-config/Cargo.toml b/yazi-config/Cargo.toml index 974090af..e1b1fc42 100644 --- a/yazi-config/Cargo.toml +++ b/yazi-config/Cargo.toml @@ -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 } diff --git a/yazi-fs/Cargo.toml b/yazi-fs/Cargo.toml index d33df641..ddd5a828 100644 --- a/yazi-fs/Cargo.toml +++ b/yazi-fs/Cargo.toml @@ -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 } diff --git a/yazi-fs/src/cwd.rs b/yazi-fs/src/cwd.rs index c5d3dd93..0b844959 100644 --- a/yazi-fs/src/cwd.rs +++ b/yazi-fs/src/cwd.rs @@ -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 = RoCell::new(); pub struct Cwd(ArcSwap); @@ -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) } } }); } diff --git a/yazi-fs/src/path/expand.rs b/yazi-fs/src/path/expand.rs index e39860c4..c3514278 100644 --- a/yazi-fs/src/path/expand.rs +++ b/yazi-fs/src/path/expand.rs @@ -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() } diff --git a/yazi-fs/src/provider/gate.rs b/yazi-fs/src/provider/gate.rs index 308ab7ee..efe979d5 100644 --- a/yazi-fs/src/provider/gate.rs +++ b/yazi-fs/src/provider/gate.rs @@ -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), diff --git a/yazi-fs/src/provider/local/gate.rs b/yazi-fs/src/provider/local/gate.rs index 2cfac5ba..322ee19f 100644 --- a/yazi-fs/src/provider/local/gate.rs +++ b/yazi-fs/src/provider/local/gate.rs @@ -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 diff --git a/yazi-fs/src/provider/local/local.rs b/yazi-fs/src/provider/local/local.rs index d8f0ef54..fa56955a 100644 --- a/yazi-fs/src/provider/local/local.rs +++ b/yazi-fs/src/provider/local/local.rs @@ -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

(&self, _: P) -> Option + async fn absolute<'a, U>(&self, url: U) -> io::Result> where - P: AsRef, + U: Into>, { - 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 { - 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? diff --git a/yazi-fs/src/provider/local/metadata.rs b/yazi-fs/src/provider/local/metadata.rs new file mode 100644 index 00000000..3f09be55 --- /dev/null +++ b/yazi-fs/src/provider/local/metadata.rs @@ -0,0 +1,40 @@ +use std::fs::{FileTimes, Permissions}; + +use crate::cha::Cha; + +impl From 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 for Permissions { + fn from(cha: Cha) -> Self { + use std::os::unix::fs::PermissionsExt; + + Permissions::from_mode(cha.mode.bits() as _) + } +} diff --git a/yazi-fs/src/provider/local/mod.rs b/yazi-fs/src/provider/local/mod.rs index 2589b059..5e0f8777 100644 --- a/yazi-fs/src/provider/local/mod.rs +++ b/yazi-fs/src/provider/local/mod.rs @@ -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); diff --git a/yazi-fs/src/provider/provider.rs b/yazi-fs/src/provider/provider.rs index fa3b5bc9..0be21fc2 100644 --- a/yazi-fs/src/provider/provider.rs +++ b/yazi-fs/src/provider/provider.rs @@ -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> +where + U: Into>, +{ + let url: Url = url.into(); + Providers::new(url).await?.absolute(url).await +} -#[inline] pub fn cache<'a, U>(url: U) -> Option where U: Into>, { - 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 where U: Into>, @@ -25,19 +47,22 @@ where } } -#[inline] pub async fn canonicalize<'a, U>(url: U) -> io::Result where U: Into>, { - 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 where U: Into>, @@ -49,69 +74,70 @@ where } } -#[inline] pub async fn copy<'a, U, V>(from: U, to: V, cha: Cha) -> io::Result where U: Into>, V: Into>, { - 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 where U: Into>, { - 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>, { - 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>, { - 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>, V: Into>, { - 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 where U: Into>, @@ -124,19 +150,14 @@ where } } -#[inline] pub async fn metadata<'a, U>(url: U) -> io::Result where U: Into>, { - 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>, @@ -145,155 +166,105 @@ where identical(a, b).await.unwrap_or(false) } -#[inline] pub async fn read_dir<'a, U>(url: U) -> io::Result where U: Into>, { 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 where U: Into>, { - 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>, { - 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>, { - 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>, { - 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>, V: Into>, { - 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>, F: AsyncFnOnce() -> io::Result, { - 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>, { - 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>, { - 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 where U: Into>, { - 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>, { - 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>, 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 } diff --git a/yazi-fs/src/provider/providers.rs b/yazi-fs/src/provider/providers.rs index ddd4516b..83323f83 100644 --- a/yazi-fs/src/provider/providers.rs +++ b/yazi-fs/src/provider/providers.rs @@ -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

(&self, path: P) -> Option + async fn absolute<'a, U>(&self, url: U) -> io::Result> where - P: AsRef, + U: Into>, { 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, } } diff --git a/yazi-fs/src/provider/rw_file.rs b/yazi-fs/src/provider/rw_file.rs index 499c7d36..43476c45 100644 --- a/yazi-fs/src/provider/rw_file.rs +++ b/yazi-fs/src/provider/rw_file.rs @@ -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), @@ -15,13 +17,34 @@ impl From 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::task::Poll> { 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> { + ) -> std::task::Poll> { 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> { + ) -> std::task::Poll> { 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> { + ) -> std::task::Poll> { 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> { + bufs: &[io::IoSlice<'_>], + ) -> std::task::Poll> { 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), diff --git a/yazi-fs/src/provider/sftp/gate.rs b/yazi-fs/src/provider/sftp/gate.rs index 6a345b76..1cb5282b 100644 --- a/yazi-fs/src/provider/sftp/gate.rs +++ b/yazi-fs/src/provider/sftp/gate.rs @@ -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, 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 { diff --git a/yazi-fs/src/provider/sftp/sftp.rs b/yazi-fs/src/provider/sftp/sftp.rs index a354e142..748ab505 100644 --- a/yazi-fs/src/provider/sftp/sftp.rs +++ b/yazi-fs/src/provider/sftp/sftp.rs @@ -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

(&self, _: P) -> Option + async fn absolute<'a, U>(&self, url: U) -> io::Result> where - P: AsRef, + U: Into>, { - 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

(&self, path: P) -> io::Result @@ -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

(&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 { - todo!() - // async fn inner(sftp: Sftp) -> anyhow::Result { - // 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 { - // FIXME - Ok(()) + if obj.is_closed() { + Err(deadpool::managed::RecycleError::Message("Channel closed".into())) + } else { + Ok(()) + } + } +} + +impl Sftp { + async fn connect(self) -> Result, 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, + ) -> Result, 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, + ) -> Result, 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, + ) -> Result, 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())) } } diff --git a/yazi-fs/src/provider/traits.rs b/yazi-fs/src/provider/traits.rs index 72db8ee1..25d4ce1b 100644 --- a/yazi-fs/src/provider/traits.rs +++ b/yazi-fs/src/provider/traits.rs @@ -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; type ReadDir: DirReader; - fn cache

(&self, path: P) -> Option + fn absolute<'a, U>(&self, url: U) -> impl Future>> where - P: AsRef; + U: Into>; fn canonicalize

(&self, path: P) -> impl Future> where @@ -40,26 +40,37 @@ pub trait Provider { P: AsRef, { 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; diff --git a/yazi-sftp/src/fs/attrs.rs b/yazi-sftp/src/fs/attrs.rs index e89f5adf..24e6b582 100644 --- a/yazi-sftp/src/fs/attrs.rs +++ b/yazi-sftp/src/fs/attrs.rs @@ -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, pub uid: Option, diff --git a/yazi-sftp/src/fs/file.rs b/yazi-sftp/src/fs/file.rs index bf717c98..fca67713 100644 --- a/yazi-sftp/src/fs/file.rs +++ b/yazi-sftp/src/fs/file.rs @@ -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 } } diff --git a/yazi-sftp/src/operator.rs b/yazi-sftp/src/operator.rs index bbfd7196..05dc6934 100644 --- a/yazi-sftp/src/operator.rs +++ b/yazi-sftp/src/operator.rs @@ -26,7 +26,7 @@ impl Operator { Ok(()) } - pub async fn open<'a, P>(&self, path: P, flags: Flags, attrs: Attrs) -> Result + pub async fn open<'a, P>(&self, path: P, flags: Flags, attrs: &'a Attrs) -> Result where P: Into>, { @@ -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() } diff --git a/yazi-sftp/src/packet.rs b/yazi-sftp/src/packet.rs index 9e5e4b49..70545f89 100644 --- a/yazi-sftp/src/packet.rs +++ b/yazi-sftp/src/packet.rs @@ -259,6 +259,7 @@ pub fn from_bytes(mut bytes: &[u8]) -> Result, 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")), }) diff --git a/yazi-sftp/src/requests/open.rs b/yazi-sftp/src/requests/open.rs index 25dd4d00..48356fa9 100644 --- a/yazi-sftp/src/requests/open.rs +++ b/yazi-sftp/src/requests/open.rs @@ -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

(path: P, flags: Flags, attrs: Attrs) -> Self + pub fn new

(path: P, flags: Flags, attrs: &'a Attrs) -> Self where P: Into>, { - Self { id: 0, path: path.into(), flags, attrs } + Self { id: 0, path: path.into(), flags, attrs: Cow::Borrowed(attrs) } } pub fn len(&self) -> usize { diff --git a/yazi-sftp/src/requests/set_stat.rs b/yazi-sftp/src/requests/set_stat.rs index fd426d7c..073350cc 100644 --- a/yazi-sftp/src/requests/set_stat.rs +++ b/yazi-sftp/src/requests/set_stat.rs @@ -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>, attrs: Attrs) -> Self { - Self { id: 0, handle: handle.into(), attrs } + pub fn new(handle: impl Into>, 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() } diff --git a/yazi-sftp/src/session.rs b/yazi-sftp/src/session.rs index 4df83f5e..2bab5855 100644 --- a/yazi-sftp/src/session.rs +++ b/yazi-sftp/src/session.rs @@ -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>, } +impl Drop for Session { + fn drop(&mut self) { self.tx.send(vec![]).ok(); } +} + impl Session { pub(super) fn make(stream: ChannelStream) -> Arc { let (tx, mut rx) = mpsc::unbounded_channel(); @@ -24,15 +28,30 @@ impl Session { extensions: Default::default(), }); + async fn read(reader: &mut ReadHalf>) -> io::Result> { + 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>, buf: Vec) -> 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() } } diff --git a/yazi-shared/src/env.rs b/yazi-shared/src/env.rs index 77f9498d..bc6a9c2c 100644 --- a/yazi-shared/src/env.rs +++ b/yazi-shared/src/env.rs @@ -53,11 +53,11 @@ impl From for LogLevel { impl AsRef 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", } } } diff --git a/yazi-shared/src/loc/loc.rs b/yazi-shared/src/loc/loc.rs index aea13d2e..94c50eab 100644 --- a/yazi-shared/src/loc/loc.rs +++ b/yazi-shared/src/loc/loc.rs @@ -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() } } diff --git a/yazi-shared/src/url/encode.rs b/yazi-shared/src/url/encode.rs index e24e756f..6f404b9c 100644 --- a/yazi-shared/src/url/encode.rs +++ b/yazi-shared/src/url/encode.rs @@ -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) } diff --git a/yazi-vfs/Cargo.toml b/yazi-vfs/Cargo.toml index ba14c226..909b45e7 100644 --- a/yazi-vfs/Cargo.toml +++ b/yazi-vfs/Cargo.toml @@ -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 } diff --git a/yazi-vfs/src/config/provider.rs b/yazi-vfs/src/config/provider.rs index ab3d4fe9..e21a7700 100644 --- a/yazi-vfs/src/config/provider.rs +++ b/yazi-vfs/src/config/provider.rs @@ -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 { + 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, - pub key_file: Option, + pub host: String, + pub user: String, + pub port: u16, + pub password: Option, + pub key_file: Option, + pub key_passphrase: Option, + // FIXME: set default: $SSH_AUTH_SOCK + pub identity_agent: Option, +} + +impl ProviderSftp { + fn reshape(self) -> io::Result { + // FIXME: expand the path + // if let Some(key_file) = self.key_file {} + // if let Some(identity_agent) = self.identity_agent {} + + Ok(self) + } } diff --git a/yazi-vfs/src/config/vfs.rs b/yazi-vfs/src/config/vfs.rs index 70cb3283..2530574c 100644 --- a/yazi-vfs/src/config/vfs.rs +++ b/yazi-vfs/src/config/vfs.rs @@ -40,14 +40,14 @@ impl Vfs { } } - pub(crate) fn read() -> io::Result { + fn read() -> io::Result { 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 { + fn reshape(self) -> io::Result { 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")))?;