From 6cfa92f11205d212155579b5b76d4cbabe723829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Wed, 17 Sep 2025 08:34:35 +0800 Subject: [PATCH] feat: SFTP virtual filesystem provider (#3172) --- Cargo.lock | 39 +++ yazi-actor/src/cmp/trigger.rs | 2 +- yazi-actor/src/mgr/bulk_rename.rs | 2 +- yazi-adapter/src/image.rs | 2 +- yazi-binding/src/cha.rs | 19 +- yazi-cli/src/package/delete.rs | 4 +- yazi-cli/src/package/dependency.rs | 6 +- yazi-cli/src/package/deploy.rs | 6 +- yazi-cli/src/package/hash.rs | 6 +- yazi-cli/src/package/package.rs | 2 +- yazi-cli/src/shared/shared.rs | 2 +- yazi-dds/src/state.rs | 2 +- yazi-dds/src/stream.rs | 2 + yazi-fm/src/app/commands/quit.rs | 2 +- yazi-fs/Cargo.toml | 2 + yazi-fs/src/cha/cha.rs | 52 ++- yazi-fs/src/lib.rs | 4 +- yazi-fs/src/mounts/linux.rs | 17 +- yazi-fs/src/mounts/macos.rs | 7 +- yazi-fs/src/mounts/partition.rs | 2 +- yazi-fs/src/mounts/partitions.rs | 6 +- yazi-fs/src/provider/dir_entry.rs | 14 +- yazi-fs/src/provider/local/dir_entry.rs | 29 +- yazi-fs/src/provider/local/gate.rs | 47 ++- yazi-fs/src/provider/local/local.rs | 382 ++++++++++----------- yazi-fs/src/provider/local/mod.rs | 2 +- yazi-fs/src/provider/local/read_dir.rs | 18 +- yazi-fs/src/provider/local/rw_file.rs | 78 ----- yazi-fs/src/provider/mod.rs | 4 +- yazi-fs/src/provider/provider.rs | 14 +- yazi-fs/src/provider/read_dir.rs | 7 +- yazi-fs/src/provider/rw_file.rs | 29 +- yazi-fs/src/provider/sftp/gate.rs | 75 ++++ yazi-fs/src/provider/sftp/mod.rs | 7 +- yazi-fs/src/provider/sftp/read_dir.rs | 26 ++ yazi-fs/src/provider/sftp/sftp.rs | 181 +++++----- yazi-fs/src/provider/traits.rs | 214 ++++++++++++ yazi-macro/src/fs.rs | 13 + yazi-macro/src/lib.rs | 1 + yazi-plugin/preset/components/linemode.lua | 6 +- yazi-plugin/src/external/highlighter.rs | 4 +- yazi-sftp/src/byte_str.rs | 9 + yazi-sftp/src/fs/file.rs | 47 +-- yazi-sftp/src/fs/read_dir.rs | 24 +- yazi-sftp/src/lib.rs | 2 + yazi-sftp/src/operator.rs | 197 +++++++++++ yazi-sftp/src/session.rs | 213 ++---------- yazi-shared/src/url/cow.rs | 4 +- yazi-watcher/src/backend/backend.rs | 2 +- 49 files changed, 1086 insertions(+), 749 deletions(-) delete mode 100644 yazi-fs/src/provider/local/rw_file.rs create mode 100644 yazi-fs/src/provider/sftp/gate.rs create mode 100644 yazi-fs/src/provider/sftp/read_dir.rs create mode 100644 yazi-fs/src/provider/traits.rs create mode 100644 yazi-macro/src/fs.rs create mode 100644 yazi-sftp/src/operator.rs diff --git a/Cargo.lock b/Cargo.lock index 682b3b51..892b87a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -874,6 +874,27 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +dependencies = [ + "tokio", +] + [[package]] name = "delegate" version = "0.13.4" @@ -1483,6 +1504,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -2185,6 +2212,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -4810,6 +4847,7 @@ dependencies = [ "arc-swap", "bitflags 2.9.4", "core-foundation-sys", + "deadpool", "dirs", "foldhash 0.2.0", "futures", @@ -4827,6 +4865,7 @@ dependencies = [ "windows-sys 0.61.0", "yazi-ffi", "yazi-macro", + "yazi-sftp", "yazi-shared", ] diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index 066a6f67..3d1c7fe0 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -47,7 +47,7 @@ impl Actor for Trigger { while let Ok(Some(ent)) = dir.next_entry().await { if let Ok(ft) = ent.file_type().await { - cache.push(CmpItem { name: ent.file_name(), is_dir: ft.is_dir() }); + cache.push(CmpItem { name: ent.name().into_owned(), is_dir: ft.is_dir() }); } } diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index 0bbec3fe..71fbdfea 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -7,7 +7,7 @@ use scopeguard::defer; use tokio::io::AsyncWriteExt; use yazi_config::{YAZI, opener::OpenerRule}; use yazi_dds::Pubsub; -use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, path::skip_url, provider::{self, local::{Gate, Local}}}; +use yazi_fs::{File, FilesOp, max_common_root, maybe_exists, path::skip_url, provider::{self, FileBuilder, Provider, local::{Gate, Local}}}; use yazi_macro::{err, succ}; use yazi_parser::VoidOpt; use yazi_proxy::{AppProxy, HIDER, TasksProxy}; diff --git a/yazi-adapter/src/image.rs b/yazi-adapter/src/image.rs index 1805d723..4ae10658 100644 --- a/yazi-adapter/src/image.rs +++ b/yazi-adapter/src/image.rs @@ -4,7 +4,7 @@ use anyhow::Result; use image::{DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation}; use ratatui::layout::Rect; use yazi_config::YAZI; -use yazi_fs::provider::local::Local; +use yazi_fs::provider::{Provider, local::Local}; use crate::Dimension; diff --git a/yazi-binding/src/cha.rs b/yazi-binding/src/cha.rs index c110322c..8502603f 100644 --- a/yazi-binding/src/cha.rs +++ b/yazi-binding/src/cha.rs @@ -37,16 +37,11 @@ impl Cha { len: t.raw_get("len").unwrap_or_default(), atime: parse_time(t.raw_get("atime").ok())?, btime: parse_time(t.raw_get("btime").ok())?, - #[cfg(unix)] ctime: parse_time(t.raw_get("ctime").ok())?, mtime: parse_time(t.raw_get("mtime").ok())?, - #[cfg(unix)] dev: t.raw_get("dev").unwrap_or_default(), - #[cfg(unix)] uid: t.raw_get("uid").unwrap_or_default(), - #[cfg(unix)] gid: t.raw_get("gid").unwrap_or_default(), - #[cfg(unix)] nlink: t.raw_get("nlink").unwrap_or_default(), }) .into_lua(lua) @@ -70,15 +65,6 @@ impl UserData for Cha { fields.add_field_method_get("is_exec", |_, me| Ok(me.is_exec())); fields.add_field_method_get("is_sticky", |_, me| Ok(me.is_sticky())); - #[cfg(unix)] - { - use std::ops::Not; - fields.add_field_method_get("dev", |_, me| Ok(me.is_dummy().not().then_some(me.dev))); - fields.add_field_method_get("uid", |_, me| Ok(me.is_dummy().not().then_some(me.uid))); - fields.add_field_method_get("gid", |_, me| Ok(me.is_dummy().not().then_some(me.gid))); - fields.add_field_method_get("nlink", |_, me| Ok(me.is_dummy().not().then_some(me.nlink))); - } - fields.add_field_method_get("len", |_, me| Ok(me.len)); fields.add_field_method_get("atime", |_, me| { Ok(me.atime.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())) @@ -86,13 +72,16 @@ impl UserData for Cha { fields.add_field_method_get("btime", |_, me| { Ok(me.btime.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())) }); - #[cfg(unix)] fields.add_field_method_get("ctime", |_, me| { Ok(me.ctime.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())) }); fields.add_field_method_get("mtime", |_, me| { Ok(me.mtime.and_then(|t| t.duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).ok())) }); + fields.add_field_method_get("dev", |_, me| Ok(me.dev)); + fields.add_field_method_get("uid", |_, me| Ok(me.uid)); + fields.add_field_method_get("gid", |_, me| Ok(me.gid)); + fields.add_field_method_get("nlink", |_, me| Ok(me.nlink)); } fn add_methods>(methods: &mut M) { diff --git a/yazi-cli/src/package/delete.rs b/yazi-cli/src/package/delete.rs index 3bdc6a90..a57428c9 100644 --- a/yazi-cli/src/package/delete.rs +++ b/yazi-cli/src/package/delete.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use yazi_fs::{ok_or_not_found, provider::local::Local, remove_dir_clean}; +use yazi_fs::{ok_or_not_found, provider::{DirReader, FileHolder, Provider, local::Local}, remove_dir_clean}; use yazi_macro::outln; use super::Dependency; @@ -25,7 +25,7 @@ impl Dependency { let assets = self.target().join("assets"); match Local::read_dir(&assets).await { Ok(mut it) => { - while let Some(entry) = it.next_entry().await? { + while let Some(entry) = it.next().await? { remove_sealed(&entry.path()) .await .with_context(|| format!("failed to remove `{}`", entry.path().display()))?; diff --git a/yazi-cli/src/package/dependency.rs b/yazi-cli/src/package/dependency.rs index 0d6e2c3c..0849c63f 100644 --- a/yazi-cli/src/package/dependency.rs +++ b/yazi-cli/src/package/dependency.rs @@ -3,7 +3,7 @@ use std::{io::BufWriter, path::{Path, PathBuf}, str::FromStr}; use anyhow::{Result, bail}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use twox_hash::XxHash3_128; -use yazi_fs::{Xdg, provider::local::Local}; +use yazi_fs::{Xdg, provider::{DirReader, FileHolder, Provider, local::Local}}; use yazi_shared::BytesExt; #[derive(Clone, Default)] @@ -65,8 +65,8 @@ impl Dependency { let mut it = Local::read_dir(dir).await?; let mut files: Vec = ["LICENSE", "README.md", "main.lua"].into_iter().map(Into::into).collect(); - while let Some(entry) = it.next_entry().await? { - if let Ok(name) = entry.file_name().into_string() + while let Some(entry) = it.next().await? { + if let Ok(name) = entry.name().into_owned().into_string() && let Some(stripped) = name.strip_suffix(".lua") && stripped != "main" && stripped.as_bytes().kebab_cased() diff --git a/yazi-cli/src/package/deploy.rs b/yazi-cli/src/package/deploy.rs index 7f75030d..3bba466c 100644 --- a/yazi-cli/src/package/deploy.rs +++ b/yazi-cli/src/package/deploy.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; -use yazi_fs::{provider::local::Local, remove_dir_clean}; +use yazi_fs::{provider::{DirReader, FileHolder, Provider, local::Local}, remove_dir_clean}; use yazi_macro::outln; use super::Dependency; @@ -43,8 +43,8 @@ impl Dependency { match Local::read_dir(&from).await { Ok(mut it) => { Local::create_dir_all(&to).await?; - while let Some(entry) = it.next_entry().await? { - let (src, dist) = (entry.path(), to.join(entry.file_name())); + while let Some(entry) = it.next().await? { + let (src, dist) = (entry.path(), to.join(entry.name())); copy_and_seal(&src, &dist).await.with_context(|| { format!("failed to copy `{}` to `{}`", src.display(), dist.display()) })?; diff --git a/yazi-cli/src/package/hash.rs b/yazi-cli/src/package/hash.rs index 687e7303..dc24e865 100644 --- a/yazi-cli/src/package/hash.rs +++ b/yazi-cli/src/package/hash.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result, bail}; use twox_hash::XxHash3_128; -use yazi_fs::{ok_or_not_found, provider::local::Local}; +use yazi_fs::{ok_or_not_found, provider::{DirReader, FileHolder, Provider, local::Local}}; use super::Dependency; @@ -20,8 +20,8 @@ impl Dependency { let mut assets = vec![]; match Local::read_dir(dir.join("assets")).await { Ok(mut it) => { - while let Some(entry) = it.next_entry().await? { - let Ok(name) = entry.file_name().into_string() else { + while let Some(entry) = it.next().await? { + let Ok(name) = entry.name().into_owned().into_string() else { bail!("asset path is not valid UTF-8: {}", entry.path().display()); }; assets.push((name, Local::read(entry.path()).await?)); diff --git a/yazi-cli/src/package/package.rs b/yazi-cli/src/package/package.rs index 4d2a0e2d..4d8aca9b 100644 --- a/yazi-cli/src/package/package.rs +++ b/yazi-cli/src/package/package.rs @@ -2,7 +2,7 @@ use std::{path::PathBuf, str::FromStr}; use anyhow::{Context, Result, bail}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use yazi_fs::{Xdg, provider::local::Local}; +use yazi_fs::{Xdg, provider::{Provider, local::Local}}; use yazi_macro::outln; use super::Dependency; diff --git a/yazi-cli/src/shared/shared.rs b/yazi-cli/src/shared/shared.rs index d1d1dbb0..d12be9b5 100644 --- a/yazi-cli/src/shared/shared.rs +++ b/yazi-cli/src/shared/shared.rs @@ -1,7 +1,7 @@ use std::{io, path::Path}; use tokio::io::AsyncWriteExt; -use yazi_fs::{ok_or_not_found, provider::local::{Gate, Local}}; +use yazi_fs::{ok_or_not_found, provider::{FileBuilder, Provider, local::{Gate, Local}}}; #[inline] pub async fn must_exists(path: impl AsRef) -> bool { diff --git a/yazi-dds/src/state.rs b/yazi-dds/src/state.rs index 584b3ff9..ffb6baed 100644 --- a/yazi-dds/src/state.rs +++ b/yazi-dds/src/state.rs @@ -5,7 +5,7 @@ use hashbrown::HashMap; use parking_lot::RwLock; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter}; use yazi_boot::BOOT; -use yazi_fs::provider::local::{Gate, Local}; +use yazi_fs::provider::{FileBuilder, Provider, local::{Gate, Local}}; use yazi_shared::{RoCell, timestamp_us}; use crate::CLIENTS; diff --git a/yazi-dds/src/stream.rs b/yazi-dds/src/stream.rs index bd4052b9..ed67add8 100644 --- a/yazi-dds/src/stream.rs +++ b/yazi-dds/src/stream.rs @@ -36,6 +36,8 @@ impl Stream { #[cfg(unix)] pub(super) async fn bind() -> std::io::Result { + use yazi_fs::provider::Provider; + let p = Self::socket_file(); yazi_fs::provider::local::Local::remove_file(&p).await.ok(); diff --git a/yazi-fm/src/app/commands/quit.rs b/yazi-fm/src/app/commands/quit.rs index 6ff6b155..d1fd8dfb 100644 --- a/yazi-fm/src/app/commands/quit.rs +++ b/yazi-fm/src/app/commands/quit.rs @@ -1,7 +1,7 @@ use std::ffi::OsString; use yazi_boot::ARGS; -use yazi_fs::provider::local::Local; +use yazi_fs::provider::{Provider, local::Local}; use yazi_shared::event::EventQuit; use crate::{Term, app::App}; diff --git a/yazi-fs/Cargo.toml b/yazi-fs/Cargo.toml index a813576b..8977621d 100644 --- a/yazi-fs/Cargo.toml +++ b/yazi-fs/Cargo.toml @@ -11,12 +11,14 @@ repository = "https://github.com/sxyazi/yazi" [dependencies] yazi-ffi = { path = "../yazi-ffi", version = "25.9.15" } yazi-macro = { path = "../yazi-macro", version = "25.9.15" } +yazi-sftp = { path = "../yazi-sftp", version = "0.1.0" } yazi-shared = { path = "../yazi-shared", version = "25.9.15" } # External dependencies anyhow = { workspace = true } arc-swap = "1.7.1" bitflags = { workspace = true } +deadpool = { version = "0.12.3", default-features = false, features = [ "managed", "rt_tokio_1" ] } dirs = { workspace = true } foldhash = { workspace = true } futures = { workspace = true } diff --git a/yazi-fs/src/cha/cha.rs b/yazi-fs/src/cha/cha.rs index 710c41a2..de76b3a6 100644 --- a/yazi-fs/src/cha/cha.rs +++ b/yazi-fs/src/cha/cha.rs @@ -13,16 +13,11 @@ pub struct Cha { pub len: u64, pub atime: Option, pub btime: Option, - #[cfg(unix)] pub ctime: Option, pub mtime: Option, - #[cfg(unix)] - pub dev: libc::dev_t, - #[cfg(unix)] + pub dev: u64, pub uid: u32, - #[cfg(unix)] pub gid: u32, - #[cfg(unix)] pub nlink: u64, } @@ -35,22 +30,17 @@ impl Deref for Cha { impl Default for Cha { fn default() -> Self { Self { - kind: ChaKind::DUMMY, - mode: ChaMode::empty(), - len: 0, - atime: None, - btime: None, - #[cfg(unix)] - ctime: None, - mtime: None, - #[cfg(unix)] - dev: 0, - #[cfg(unix)] - uid: 0, - #[cfg(unix)] - gid: 0, - #[cfg(unix)] - nlink: 0, + kind: ChaKind::DUMMY, + mode: ChaMode::empty(), + len: 0, + atime: None, + btime: None, + ctime: None, + mtime: None, + dev: 0, + uid: 0, + gid: 0, + nlink: 0, } } } @@ -136,17 +126,15 @@ impl Cha { len: m.len(), atime: m.accessed().ok(), btime: m.created().ok(), - #[cfg(unix)] - ctime: UNIX_EPOCH.checked_add(Duration::new(m.ctime() as u64, m.ctime_nsec() as u32)), + ctime: unix_either!( + UNIX_EPOCH.checked_add(Duration::new(m.ctime() as u64, m.ctime_nsec() as u32)), + None + ), mtime: m.modified().ok(), - #[cfg(unix)] - dev: m.dev() as _, - #[cfg(unix)] - uid: m.uid() as _, - #[cfg(unix)] - gid: m.gid() as _, - #[cfg(unix)] - nlink: m.nlink() as _, + dev: unix_either!(m.dev(), 0) as _, + uid: unix_either!(m.uid(), 0) as _, + gid: unix_either!(m.gid(), 0) as _, + nlink: unix_either!(m.nlink(), 0) as _, } } diff --git a/yazi-fs/src/lib.rs b/yazi-fs/src/lib.rs index 99f63d37..8c80d0d4 100644 --- a/yazi-fs/src/lib.rs +++ b/yazi-fs/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(clippy::if_same_then_else, clippy::option_map_unit_fn)] +#![allow(clippy::if_same_then_else, clippy::option_map_unit_fn, clippy::unit_arg)] yazi_macro::mod_pub!(cha mounts provider path); @@ -8,4 +8,6 @@ pub fn init() { CWD.init(<_>::default()); mounts::init(); + + provider::init(); } diff --git a/yazi-fs/src/mounts/linux.rs b/yazi-fs/src/mounts/linux.rs index 55e8d555..66df3ae5 100644 --- a/yazi-fs/src/mounts/linux.rs +++ b/yazi-fs/src/mounts/linux.rs @@ -9,22 +9,22 @@ use yazi_shared::{natsort, replace_cow, replace_vec_cow}; use super::{Locked, Partition, Partitions}; impl Partitions { - pub fn monitor(me: Locked, cb: F) + pub fn monitor(me: &'static Locked, cb: F) where F: Fn() + Copy + Send + 'static, { - async fn wait_mounts(me: Locked, cb: impl Fn()) -> Result<()> { + async fn wait_mounts(me: &'static Locked, cb: impl Fn()) -> Result<()> { let f = std::fs::File::open("/proc/mounts")?; let fd = AsyncFd::with_interest(f.as_fd(), Interest::READABLE)?; loop { let mut guard = fd.readable().await?; guard.clear_ready(); - Partitions::update(me.clone()).await; + Partitions::update(me).await; cb(); } } - async fn wait_partitions(me: Locked, cb: impl Fn()) -> Result<()> { + async fn wait_partitions(me: &'static Locked, cb: impl Fn()) -> Result<()> { loop { let partitions = Partitions::partitions()?; if me.read().linux_cache == partitions { @@ -33,17 +33,16 @@ impl Partitions { } me.write().linux_cache = partitions; - Partitions::update(me.clone()).await; + Partitions::update(me).await; cb(); sleep(Duration::from_secs(3)).await; } } - let me_ = me.clone(); tokio::spawn(async move { loop { - if let Err(e) = wait_mounts(me_.clone(), cb).await { + if let Err(e) = wait_mounts(me, cb).await { error!("Error encountered while monitoring /proc/mounts: {e:?}"); } sleep(Duration::from_secs(5)).await; @@ -52,7 +51,7 @@ impl Partitions { tokio::spawn(async move { loop { - if let Err(e) = wait_partitions(me.clone(), cb).await { + if let Err(e) = wait_partitions(me, cb).await { error!("Error encountered while monitoring /proc/partitions: {e:?}"); } sleep(Duration::from_secs(5)).await; @@ -75,7 +74,7 @@ impl Partitions { Ok(set) } - async fn update(me: Locked) { + async fn update(me: &'static Locked) { _ = tokio::task::spawn_blocking(move || { let mut guard = me.write(); match Self::all(&guard) { diff --git a/yazi-fs/src/mounts/macos.rs b/yazi-fs/src/mounts/macos.rs index 9d080da4..6d8367d2 100644 --- a/yazi-fs/src/mounts/macos.rs +++ b/yazi-fs/src/mounts/macos.rs @@ -12,7 +12,7 @@ use yazi_shared::natsort; use super::{Locked, Partition, Partitions}; impl Partitions { - pub fn monitor(me: Locked, cb: F) + pub fn monitor(me: &'static Locked, cb: F) where F: Fn() + Copy + Send + 'static, { @@ -39,12 +39,11 @@ impl Partitions { } let create_context = || { - let me = me.clone(); let boxed: Box = Box::new(move || { if mem::replace(&mut me.write().need_update, true) { return; } - Self::update(me.clone(), cb); + Self::update(me, cb); }); Box::into_raw(Box::new(boxed)) as *mut c_void }; @@ -70,7 +69,7 @@ impl Partitions { }); } - fn update(me: Locked, cb: impl Fn() + Send + 'static) { + fn update(me: &'static Locked, cb: impl Fn() + Send + 'static) { _ = tokio::task::spawn_blocking(move || { let result = Self::all_names().and_then(Self::all_partitions); if let Err(ref e) = result { diff --git a/yazi-fs/src/mounts/partition.rs b/yazi-fs/src/mounts/partition.rs index a27bcb54..9ba45b6f 100644 --- a/yazi-fs/src/mounts/partition.rs +++ b/yazi-fs/src/mounts/partition.rs @@ -5,7 +5,7 @@ pub struct Partition { pub src: OsString, pub dist: Option, #[cfg(unix)] - pub rdev: Option, + pub rdev: Option, pub label: Option, pub fstype: Option, pub capacity: u64, diff --git a/yazi-fs/src/mounts/partitions.rs b/yazi-fs/src/mounts/partitions.rs index 1ea6baaf..b5bb83db 100644 --- a/yazi-fs/src/mounts/partitions.rs +++ b/yazi-fs/src/mounts/partitions.rs @@ -1,4 +1,4 @@ -use std::{ops::Deref, sync::Arc}; +use std::ops::Deref; use parking_lot::RwLock; use yazi_shared::RoCell; @@ -6,7 +6,7 @@ use yazi_shared::RoCell; use super::Partition; use crate::cha::Cha; -pub(super) type Locked = Arc>; +pub(super) type Locked = RwLock; pub static PARTITIONS: RoCell = RoCell::new(); @@ -27,7 +27,7 @@ impl Deref for Partitions { impl Partitions { #[cfg(unix)] - pub fn by_dev(&self, dev: libc::dev_t) -> Option<&Partition> { + pub fn by_dev(&self, dev: u64) -> Option<&Partition> { self.inner.iter().find(|p| p.rdev == Some(dev)) } diff --git a/yazi-fs/src/provider/dir_entry.rs b/yazi-fs/src/provider/dir_entry.rs index 910a865d..df24a090 100644 --- a/yazi-fs/src/provider/dir_entry.rs +++ b/yazi-fs/src/provider/dir_entry.rs @@ -1,23 +1,29 @@ -use std::{ffi::OsString, io}; +use std::{borrow::Cow, ffi::OsStr, io}; use yazi_shared::url::UrlBuf; +use crate::provider::FileHolder; + pub enum DirEntry { Local(super::local::DirEntry), } +impl From for DirEntry { + fn from(value: super::local::DirEntry) -> Self { Self::Local(value) } +} + impl DirEntry { #[must_use] pub fn url(&self) -> UrlBuf { match self { - Self::Local(local) => local.url(), + Self::Local(local) => local.path().into(), } } #[must_use] - pub fn file_name(&self) -> OsString { + pub fn name(&self) -> Cow<'_, OsStr> { match self { - Self::Local(local) => local.file_name(), + Self::Local(local) => local.name(), } } diff --git a/yazi-fs/src/provider/local/dir_entry.rs b/yazi-fs/src/provider/local/dir_entry.rs index c6078f13..706a5675 100644 --- a/yazi-fs/src/provider/local/dir_entry.rs +++ b/yazi-fs/src/provider/local/dir_entry.rs @@ -1,24 +1,15 @@ -use std::ops::Deref; +use std::{borrow::Cow, ffi::OsStr, io, path::PathBuf}; -use yazi_shared::url::UrlBuf; +use crate::provider::FileHolder; -pub struct DirEntry(tokio::fs::DirEntry); +pub struct DirEntry(pub(super) tokio::fs::DirEntry); -impl Deref for DirEntry { - type Target = tokio::fs::DirEntry; +impl FileHolder for DirEntry { + fn path(&self) -> PathBuf { self.0.path() } - fn deref(&self) -> &Self::Target { &self.0 } -} - -impl From for DirEntry { - fn from(value: tokio::fs::DirEntry) -> Self { Self(value) } -} - -impl From for crate::provider::DirEntry { - fn from(value: DirEntry) -> Self { Self::Local(value) } -} - -impl DirEntry { - #[must_use] - pub fn url(&self) -> UrlBuf { self.0.path().into() } + fn name(&self) -> Cow<'_, OsStr> { self.0.file_name().into() } + + async fn metadata(&self) -> io::Result { self.0.metadata().await } + + async fn file_type(&self) -> io::Result { self.0.file_type().await } } diff --git a/yazi-fs/src/provider/local/gate.rs b/yazi-fs/src/provider/local/gate.rs index 4dcaf356..8f55cc9e 100644 --- a/yazi-fs/src/provider/local/gate.rs +++ b/yazi-fs/src/provider/local/gate.rs @@ -1,14 +1,47 @@ -use std::ops::{Deref, DerefMut}; +use std::{io, path::Path}; + +use crate::provider::FileBuilder; #[derive(Default)] pub struct Gate(tokio::fs::OpenOptions); -impl Deref for Gate { - type Target = tokio::fs::OpenOptions; +impl FileBuilder for Gate { + type File = tokio::fs::File; - fn deref(&self) -> &Self::Target { &self.0 } -} + fn append(&mut self, append: bool) -> &mut Self { + self.0.append(append); + self + } -impl DerefMut for Gate { - fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } + fn create(&mut self, create: bool) -> &mut Self { + self.0.create(create); + self + } + + fn create_new(&mut self, create_new: bool) -> &mut Self { + self.0.create_new(create_new); + self + } + + async fn open

(&self, path: P) -> io::Result + where + P: AsRef, + { + self.0.open(path).await + } + + fn read(&mut self, read: bool) -> &mut Self { + self.0.read(read); + self + } + + fn truncate(&mut self, truncate: bool) -> &mut Self { + self.0.truncate(truncate); + self + } + + fn write(&mut self, write: bool) -> &mut Self { + self.0.write(write); + self + } } diff --git a/yazi-fs/src/provider/local/local.rs b/yazi-fs/src/provider/local/local.rs index ac1d767f..fe078fcf 100644 --- a/yazi-fs/src/provider/local/local.rs +++ b/yazi-fs/src/provider/local/local.rs @@ -1,12 +1,16 @@ use std::{io, path::{Path, PathBuf}}; -use crate::{cha::Cha, provider::local::{Gate, ReadDir, RwFile}}; +use crate::{cha::Cha, provider::Provider}; pub struct Local; -impl Local { +impl Provider for Local { + type File = tokio::fs::File; + type Gate = super::Gate; + type ReadDir = super::ReadDir; + #[inline] - pub fn cache

(_: P) -> Option + fn cache

(_: P) -> Option where P: AsRef, { @@ -14,7 +18,7 @@ impl Local { } #[inline] - pub async fn canonicalize

(path: P) -> io::Result + async fn canonicalize

(path: P) -> io::Result where P: AsRef, { @@ -22,7 +26,7 @@ impl Local { } #[inline] - pub async fn copy(from: P, to: Q, cha: Cha) -> io::Result + async fn copy(from: P, to: Q, cha: Cha) -> io::Result where P: AsRef, Q: AsRef, @@ -32,6 +36,183 @@ impl Local { Self::copy_impl(from, to, cha).await } + #[inline] + async fn create_dir

(path: P) -> io::Result<()> + where + P: AsRef, + { + tokio::fs::create_dir(path).await + } + + #[inline] + async fn create_dir_all

(path: P) -> io::Result<()> + where + P: AsRef, + { + tokio::fs::create_dir_all(path).await + } + + #[inline] + async fn hard_link(original: P, link: Q) -> io::Result<()> + where + P: AsRef, + Q: AsRef, + { + tokio::fs::hard_link(original, link).await + } + + #[inline] + async fn metadata

(path: P) -> io::Result + where + P: AsRef, + { + tokio::fs::metadata(path).await + } + + #[inline] + async fn read_dir

(path: P) -> io::Result + where + P: AsRef, + { + tokio::fs::read_dir(path).await.map(super::ReadDir) + } + + #[inline] + async fn read_link

(path: P) -> io::Result + where + P: AsRef, + { + tokio::fs::read_link(path).await + } + + #[inline] + async fn remove_dir

(path: P) -> io::Result<()> + where + P: AsRef, + { + tokio::fs::remove_dir(path).await + } + + #[inline] + async fn remove_dir_all

(path: P) -> io::Result<()> + where + P: AsRef, + { + tokio::fs::remove_dir_all(path).await + } + + #[inline] + async fn remove_file

(path: P) -> io::Result<()> + where + P: AsRef, + { + tokio::fs::remove_file(path).await + } + + #[inline] + async fn rename(from: P, to: Q) -> io::Result<()> + where + P: AsRef, + Q: AsRef, + { + tokio::fs::rename(from, to).await + } + + #[inline] + async fn symlink(original: P, link: Q, _is_dir: F) -> io::Result<()> + where + P: AsRef, + Q: AsRef, + F: AsyncFnOnce() -> io::Result, + { + #[cfg(unix)] + { + tokio::fs::symlink(original, link).await + } + #[cfg(windows)] + if _is_dir().await? { + Self::symlink_dir(original, link).await + } else { + Self::symlink_file(original, link).await + } + } + + #[inline] + async fn symlink_dir(original: P, link: Q) -> io::Result<()> + where + P: AsRef, + Q: AsRef, + { + #[cfg(unix)] + { + tokio::fs::symlink(original, link).await + } + #[cfg(windows)] + { + tokio::fs::symlink_dir(original, link).await + } + } + + #[inline] + async fn symlink_file(original: P, link: Q) -> io::Result<()> + where + P: AsRef, + Q: AsRef, + { + #[cfg(unix)] + { + tokio::fs::symlink(original, link).await + } + #[cfg(windows)] + { + tokio::fs::symlink_file(original, link).await + } + } + + #[inline] + async fn symlink_metadata

(path: P) -> io::Result + where + P: AsRef, + { + tokio::fs::symlink_metadata(path).await + } + + async fn trash

(path: P) -> io::Result<()> + where + P: AsRef, + { + let path = path.as_ref().to_owned(); + tokio::task::spawn_blocking(move || { + #[cfg(target_os = "android")] + { + Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported OS for trash operation")) + } + #[cfg(target_os = "macos")] + { + use trash::{TrashContext, macos::{DeleteMethod, TrashContextExtMacos}}; + let mut ctx = TrashContext::default(); + ctx.set_delete_method(DeleteMethod::NsFileManager); + ctx.delete(path).map_err(io::Error::other) + } + #[cfg(all(not(target_os = "macos"), not(target_os = "android")))] + { + trash::delete(path).map_err(io::Error::other) + } + }) + .await? + } + + #[inline] + async fn write(path: P, contents: C) -> io::Result<()> + where + P: AsRef, + C: AsRef<[u8]>, + { + tokio::fs::write(path, contents).await + } +} + +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)); @@ -80,55 +261,6 @@ impl Local { } } - #[inline] - pub async fn create

(path: P) -> io::Result - where - P: AsRef, - { - Gate::default().write(true).create(true).truncate(true).open(path).await.map(Into::into) - } - - #[inline] - pub async fn create_dir

(path: P) -> io::Result<()> - where - P: AsRef, - { - tokio::fs::create_dir(path).await - } - - #[inline] - pub async fn create_dir_all

(path: P) -> io::Result<()> - where - P: AsRef, - { - tokio::fs::create_dir_all(path).await - } - - #[inline] - pub async fn hard_link(original: P, link: Q) -> io::Result<()> - where - P: AsRef, - Q: AsRef, - { - tokio::fs::hard_link(original, link).await - } - - #[inline] - pub async fn metadata

(path: P) -> io::Result - where - P: AsRef, - { - tokio::fs::metadata(path).await - } - - #[inline] - pub async fn open

(path: P) -> io::Result - where - P: AsRef, - { - Gate::default().read(true).open(path).await.map(Into::into) - } - #[inline] pub async fn read

(path: P) -> io::Result> where @@ -137,22 +269,6 @@ impl Local { tokio::fs::read(path).await } - #[inline] - pub async fn read_dir

(path: P) -> io::Result - where - P: AsRef, - { - tokio::fs::read_dir(path).await.map(Into::into) - } - - #[inline] - pub async fn read_link

(path: P) -> io::Result - where - P: AsRef, - { - tokio::fs::read_link(path).await - } - #[inline] pub async fn read_to_string

(path: P) -> io::Result where @@ -160,130 +276,4 @@ impl Local { { tokio::fs::read_to_string(path).await } - - #[inline] - pub async fn remove_dir

(path: P) -> io::Result<()> - where - P: AsRef, - { - tokio::fs::remove_dir(path).await - } - - #[inline] - pub async fn remove_dir_all

(path: P) -> io::Result<()> - where - P: AsRef, - { - tokio::fs::remove_dir_all(path).await - } - - #[inline] - pub async fn remove_file

(path: P) -> io::Result<()> - where - P: AsRef, - { - tokio::fs::remove_file(path).await - } - - #[inline] - pub async fn rename(from: P, to: Q) -> io::Result<()> - where - P: AsRef, - Q: AsRef, - { - tokio::fs::rename(from, to).await - } - - #[inline] - pub async fn symlink(original: P, link: Q, _is_dir: F) -> io::Result<()> - where - P: AsRef, - Q: AsRef, - F: AsyncFnOnce() -> io::Result, - { - #[cfg(unix)] - { - tokio::fs::symlink(original, link).await - } - #[cfg(windows)] - if _is_dir().await? { - Self::symlink_dir(original, link).await - } else { - Self::symlink_file(original, link).await - } - } - - #[inline] - pub async fn symlink_dir(original: P, link: Q) -> io::Result<()> - where - P: AsRef, - Q: AsRef, - { - #[cfg(unix)] - { - tokio::fs::symlink(original, link).await - } - #[cfg(windows)] - { - tokio::fs::symlink_dir(original, link).await - } - } - - #[inline] - pub async fn symlink_file(original: P, link: Q) -> io::Result<()> - where - P: AsRef, - Q: AsRef, - { - #[cfg(unix)] - { - tokio::fs::symlink(original, link).await - } - #[cfg(windows)] - { - tokio::fs::symlink_file(original, link).await - } - } - - #[inline] - pub async fn symlink_metadata

(path: P) -> io::Result - where - P: AsRef, - { - tokio::fs::symlink_metadata(path).await - } - - pub async fn trash

(path: P) -> io::Result<()> - where - P: AsRef, - { - let path = path.as_ref().to_owned(); - tokio::task::spawn_blocking(move || { - #[cfg(target_os = "android")] - { - Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported OS for trash operation")) - } - #[cfg(target_os = "macos")] - { - use trash::{TrashContext, macos::{DeleteMethod, TrashContextExtMacos}}; - let mut ctx = TrashContext::default(); - ctx.set_delete_method(DeleteMethod::NsFileManager); - ctx.delete(path).map_err(io::Error::other) - } - #[cfg(all(not(target_os = "macos"), not(target_os = "android")))] - { - trash::delete(path).map_err(io::Error::other) - } - }) - .await? - } - - #[inline] - pub async fn write(path: P, contents: C) -> io::Result<()> - where - P: AsRef, - C: AsRef<[u8]>, - { - tokio::fs::write(path, contents).await - } } diff --git a/yazi-fs/src/provider/local/mod.rs b/yazi-fs/src/provider/local/mod.rs index 32072894..2589b059 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 rw_file); +yazi_macro::mod_flat!(calculator casefold dir_entry gate identical local read_dir); diff --git a/yazi-fs/src/provider/local/read_dir.rs b/yazi-fs/src/provider/local/read_dir.rs index 1aa34bdc..ab1b90d9 100644 --- a/yazi-fs/src/provider/local/read_dir.rs +++ b/yazi-fs/src/provider/local/read_dir.rs @@ -1,19 +1,13 @@ use std::io; -use super::DirEntry; +use crate::provider::DirReader; -pub struct ReadDir(tokio::fs::ReadDir); +pub struct ReadDir(pub(super) tokio::fs::ReadDir); -impl From for ReadDir { - fn from(value: tokio::fs::ReadDir) -> Self { Self(value) } -} +impl DirReader for ReadDir { + type Entry<'a> = super::DirEntry; -impl From for crate::provider::ReadDir { - fn from(value: ReadDir) -> Self { Self::Local(value) } -} - -impl ReadDir { - pub async fn next_entry(&mut self) -> io::Result> { - self.0.next_entry().await.map(|entry| entry.map(Into::into)) + async fn next(&mut self) -> io::Result>> { + self.0.next_entry().await.map(|entry| entry.map(super::DirEntry)) } } diff --git a/yazi-fs/src/provider/local/rw_file.rs b/yazi-fs/src/provider/local/rw_file.rs deleted file mode 100644 index 892c4db3..00000000 --- a/yazi-fs/src/provider/local/rw_file.rs +++ /dev/null @@ -1,78 +0,0 @@ -use std::{pin::Pin, task::Poll}; - -use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite}; - -pub struct RwFile(tokio::fs::File); - -impl From for RwFile { - fn from(value: tokio::fs::File) -> Self { Self(value) } -} - -impl From for crate::provider::RwFile { - fn from(value: RwFile) -> Self { Self::Local(value) } -} - -impl AsyncRead for RwFile { - #[inline] - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &mut tokio::io::ReadBuf<'_>, - ) -> Poll> { - Pin::new(&mut self.0).poll_read(cx, buf) - } -} - -impl AsyncSeek for RwFile { - #[inline] - fn start_seek(mut self: Pin<&mut Self>, position: std::io::SeekFrom) -> std::io::Result<()> { - Pin::new(&mut self.0).start_seek(position) - } - - #[inline] - fn poll_complete( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> Poll> { - Pin::new(&mut self.0).poll_complete(cx) - } -} - -impl AsyncWrite for RwFile { - #[inline] - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &[u8], - ) -> Poll> { - Pin::new(&mut self.0).poll_write(cx, buf) - } - - #[inline] - fn poll_flush( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> Poll> { - Pin::new(&mut self.0).poll_flush(cx) - } - - #[inline] - fn poll_shutdown( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> Poll> { - Pin::new(&mut self.0).poll_shutdown(cx) - } - - #[inline] - fn poll_write_vectored( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - bufs: &[std::io::IoSlice<'_>], - ) -> Poll> { - Pin::new(&mut self.0).poll_write_vectored(cx, bufs) - } - - #[inline] - fn is_write_vectored(&self) -> bool { self.0.is_write_vectored() } -} diff --git a/yazi-fs/src/provider/mod.rs b/yazi-fs/src/provider/mod.rs index 68cf430b..a303ec3e 100644 --- a/yazi-fs/src/provider/mod.rs +++ b/yazi-fs/src/provider/mod.rs @@ -1,3 +1,5 @@ yazi_macro::mod_pub!(local sftp); -yazi_macro::mod_flat!(calculator dir_entry provider read_dir rw_file); +yazi_macro::mod_flat!(calculator dir_entry provider read_dir rw_file traits); + +pub fn init() { sftp::init(); } diff --git a/yazi-fs/src/provider/provider.rs b/yazi-fs/src/provider/provider.rs index c0cd1503..197123ac 100644 --- a/yazi-fs/src/provider/provider.rs +++ b/yazi-fs/src/provider/provider.rs @@ -2,7 +2,7 @@ use std::{io, path::{Path, PathBuf}}; use yazi_shared::url::{Url, UrlBuf}; -use crate::{cha::Cha, provider::{ReadDir, RwFile, local::{self, Local}}}; +use crate::{cha::Cha, provider::{Provider, ReadDir, RwFile, local::{self, Local}}}; #[inline] pub fn cache<'a, U>(url: U) -> Option @@ -145,18 +145,6 @@ where identical(a, b).await.unwrap_or(false) } -#[inline] -pub async fn open<'a, U>(url: U) -> io::Result -where - U: Into>, -{ - if let Some(path) = url.into().as_path() { - Local::open(path).await.map(Into::into) - } else { - Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) - } -} - #[inline] pub async fn read_dir<'a, U>(url: U) -> io::Result where diff --git a/yazi-fs/src/provider/read_dir.rs b/yazi-fs/src/provider/read_dir.rs index 0a1972d9..5e008e19 100644 --- a/yazi-fs/src/provider/read_dir.rs +++ b/yazi-fs/src/provider/read_dir.rs @@ -1,15 +1,20 @@ use std::io; use super::DirEntry; +use crate::provider::DirReader; pub enum ReadDir { Local(super::local::ReadDir), } +impl From for ReadDir { + fn from(value: super::local::ReadDir) -> Self { Self::Local(value) } +} + impl ReadDir { pub async fn next_entry(&mut self) -> io::Result> { match self { - Self::Local(local) => local.next_entry().await.map(|entry| entry.map(Into::into)), + Self::Local(local) => local.next().await.map(|entry| entry.map(Into::into)), } } } diff --git a/yazi-fs/src/provider/rw_file.rs b/yazi-fs/src/provider/rw_file.rs index 0156fd6f..69a74571 100644 --- a/yazi-fs/src/provider/rw_file.rs +++ b/yazi-fs/src/provider/rw_file.rs @@ -3,7 +3,16 @@ use std::pin::Pin; use tokio::io::{AsyncRead, AsyncWrite}; pub enum RwFile { - Local(super::local::RwFile), + Tokio(tokio::fs::File), + SFTP(Box), +} + +impl From for RwFile { + fn from(f: tokio::fs::File) -> Self { Self::Tokio(f) } +} + +impl From for RwFile { + fn from(f: yazi_sftp::fs::File) -> Self { Self::SFTP(Box::new(f)) } } impl AsyncRead for RwFile { @@ -14,7 +23,8 @@ impl AsyncRead for RwFile { buf: &mut tokio::io::ReadBuf<'_>, ) -> std::task::Poll> { match &mut *self { - Self::Local(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), } } } @@ -27,7 +37,8 @@ impl AsyncWrite for RwFile { buf: &[u8], ) -> std::task::Poll> { match &mut *self { - Self::Local(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), } } @@ -37,7 +48,8 @@ impl AsyncWrite for RwFile { cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { match &mut *self { - Self::Local(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), } } @@ -47,7 +59,8 @@ impl AsyncWrite for RwFile { cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { match &mut *self { - Self::Local(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), } } @@ -58,14 +71,16 @@ impl AsyncWrite for RwFile { bufs: &[std::io::IoSlice<'_>], ) -> std::task::Poll> { match &mut *self { - Self::Local(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), } } #[inline] fn is_write_vectored(&self) -> bool { match self { - Self::Local(f) => f.is_write_vectored(), + Self::Tokio(f) => f.is_write_vectored(), + Self::SFTP(f) => f.is_write_vectored(), } } } diff --git a/yazi-fs/src/provider/sftp/gate.rs b/yazi-fs/src/provider/sftp/gate.rs new file mode 100644 index 00000000..d35b17ac --- /dev/null +++ b/yazi-fs/src/provider/sftp/gate.rs @@ -0,0 +1,75 @@ +use std::{io, path::Path}; + +use yazi_sftp::fs::{Attrs, Flags}; + +use crate::provider::FileBuilder; + +#[derive(Default)] +pub struct Gate { + append: bool, + create: bool, + create_new: bool, + read: bool, + truncate: bool, + write: bool, +} + +impl FileBuilder for Gate { + type File = yazi_sftp::fs::File; + + fn append(&mut self, append: bool) -> &mut Self { + self.append = append; + self + } + + fn create(&mut self, create: bool) -> &mut Self { + self.create = create; + self + } + + fn create_new(&mut self, create_new: bool) -> &mut Self { + self.create_new = create_new; + self + } + + async fn open

(&self, path: P) -> io::Result + where + P: AsRef, + { + let mut flags = Flags::empty(); + if self.append { + flags |= Flags::APPEND; + } + if self.create { + flags |= Flags::CREATE; + } + if self.create_new { + flags |= Flags::CREATE | Flags::EXCLUDE; + } + if self.read { + flags |= Flags::READ; + } + if self.truncate { + flags |= Flags::TRUNCATE; + } + if self.write { + flags |= Flags::WRITE; + } + Ok(super::Sftp::op().await?.open(&path, flags, Attrs::default()).await?) + } + + fn read(&mut self, read: bool) -> &mut Self { + self.read = read; + self + } + + fn truncate(&mut self, truncate: bool) -> &mut Self { + self.truncate = truncate; + self + } + + fn write(&mut self, write: bool) -> &mut Self { + self.write = write; + self + } +} diff --git a/yazi-fs/src/provider/sftp/mod.rs b/yazi-fs/src/provider/sftp/mod.rs index 330ec80f..b88864b7 100644 --- a/yazi-fs/src/provider/sftp/mod.rs +++ b/yazi-fs/src/provider/sftp/mod.rs @@ -1 +1,6 @@ -yazi_macro::mod_flat!(sftp); +yazi_macro::mod_flat!(gate read_dir sftp); + +pub(super) static CONN: yazi_shared::RoCell> = + yazi_shared::RoCell::new(); + +pub(super) fn init() { CONN.init(deadpool::managed::Pool::builder(Sftp).build().unwrap()); } diff --git a/yazi-fs/src/provider/sftp/read_dir.rs b/yazi-fs/src/provider/sftp/read_dir.rs new file mode 100644 index 00000000..be753008 --- /dev/null +++ b/yazi-fs/src/provider/sftp/read_dir.rs @@ -0,0 +1,26 @@ +use std::{borrow::Cow, ffi::OsStr, io, path::PathBuf}; + +use crate::provider::{DirReader, FileHolder}; + +pub struct ReadDir(pub(super) yazi_sftp::fs::ReadDir); + +impl DirReader for ReadDir { + type Entry<'a> = DirEntry<'a>; + + async fn next(&mut self) -> io::Result>> { + Ok(self.0.next().await?.map(DirEntry)) + } +} + +// --- Entry +pub struct DirEntry<'a>(yazi_sftp::fs::DirEntry<'a>); + +impl FileHolder for DirEntry<'_> { + fn path(&self) -> PathBuf { self.0.path() } + + fn name(&self) -> Cow<'_, OsStr> { self.0.name() } + + async fn metadata(&self) -> io::Result { todo!() } + + async fn file_type(&self) -> io::Result { todo!() } +} diff --git a/yazi-fs/src/provider/sftp/sftp.rs b/yazi-fs/src/provider/sftp/sftp.rs index a3f030ba..c77b4875 100644 --- a/yazi-fs/src/provider/sftp/sftp.rs +++ b/yazi-fs/src/provider/sftp/sftp.rs @@ -1,176 +1,169 @@ -use std::{io, path::{Path, PathBuf}}; +use std::{io, path::{Path, PathBuf}, time::UNIX_EPOCH}; -use crate::cha::Cha; +use yazi_sftp::fs::{Attrs, Flags}; + +use crate::{cha::Cha, provider::Provider}; pub struct Sftp; -impl Sftp { - pub fn cache

(_: P) -> Option +impl Provider for Sftp { + type File = yazi_sftp::fs::File; + type Gate = super::Gate; + type ReadDir = super::ReadDir; + + fn cache

(_: P) -> Option where P: AsRef, { todo!() } - pub async fn canonicalize

(path: P) -> io::Result + async fn canonicalize

(path: P) -> io::Result where P: AsRef, { - todo!() + Ok(Self::op().await?.realpath(&path).await?) } - pub async fn copy(from: P, to: Q, cha: Cha) -> io::Result + async fn copy(from: P, to: Q, cha: Cha) -> io::Result where P: AsRef, Q: AsRef, { - todo!() + // FIXME: pull this out to a From for Attrs impl + let attrs = Attrs { + size: Some(cha.len), + uid: Some(cha.uid), + gid: Some(cha.gid), + perm: Some(cha.mode.bits() as _), + atime: cha + .atime + .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) + .map(|d| d.as_secs() as u32), + mtime: cha + .mtime + .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) + .map(|d| d.as_secs() as u32), + extended: Default::default(), + }; + + let op = Self::op().await?; + + let mut from = op.open(&from, Flags::READ, Attrs::default()).await?; + let mut to = op.open(&to, Flags::WRITE | Flags::CREATE | Flags::TRUNCATE, attrs).await?; + + tokio::io::copy(&mut from, &mut to).await } - pub async fn create

(path: P) -> io::Result<()> + async fn create_dir

(path: P) -> io::Result<()> where P: AsRef, { - todo!() + Ok(Self::op().await?.mkdir(&path, Attrs::default()).await?) } - pub async fn create_dir

(path: P) -> io::Result<()> - where - P: AsRef, - { - todo!() - } - - pub async fn create_dir_all

(path: P) -> io::Result<()> - where - P: AsRef, - { - todo!() - } - - pub async fn hard_link(original: P, link: Q) -> io::Result<()> + async fn hard_link(original: P, link: Q) -> io::Result<()> where P: AsRef, Q: AsRef, { - todo!() + Ok(Self::op().await?.hardlink(&original, &link).await?) } - pub async fn metadata

(path: P) -> io::Result + async fn metadata

(path: P) -> io::Result where P: AsRef, { todo!() } - pub async fn open

(path: P) -> io::Result<()> + async fn read_dir

(path: P) -> io::Result where P: AsRef, { - todo!() + Ok(super::ReadDir(Self::op().await?.read_dir(&path).await?)) } - pub async fn read

(path: P) -> io::Result> + async fn read_link

(path: P) -> io::Result where P: AsRef, { - todo!() + Ok(Self::op().await?.readlink(&path).await?) } - pub async fn read_dir

(path: P) -> io::Result<()> + async fn remove_dir

(path: P) -> io::Result<()> where P: AsRef, { - todo!() + Ok(Self::op().await?.rmdir(&path).await?) } - pub async fn read_link

(path: P) -> io::Result + async fn remove_file

(path: P) -> io::Result<()> where P: AsRef, { - todo!() + Ok(Self::op().await?.remove(&path).await?) } - pub async fn read_to_string

(path: P) -> io::Result - where - P: AsRef, - { - todo!() - } - - pub async fn remove_dir

(path: P) -> io::Result<()> - where - P: AsRef, - { - todo!() - } - - pub async fn remove_dir_all

(path: P) -> io::Result<()> - where - P: AsRef, - { - todo!() - } - - pub async fn remove_file

(path: P) -> io::Result<()> - where - P: AsRef, - { - todo!() - } - - pub async fn rename(from: P, to: Q) -> io::Result<()> + async fn rename(from: P, to: Q) -> io::Result<()> where P: AsRef, Q: AsRef, { - todo!() + Ok(Self::op().await?.rename(&from, &to).await?) } - pub async fn symlink(original: P, link: Q, _is_dir: F) -> io::Result<()> + async fn symlink(original: P, link: Q, _is_dir: F) -> io::Result<()> where P: AsRef, Q: AsRef, F: AsyncFnOnce() -> io::Result, { - todo!() + Ok(Self::op().await?.symlink(&original, &link).await?) } - pub async fn symlink_dir(original: P, link: Q) -> io::Result<()> - where - P: AsRef, - Q: AsRef, - { - todo!() - } - - pub async fn symlink_file(original: P, link: Q) -> io::Result<()> - where - P: AsRef, - Q: AsRef, - { - todo!() - } - - pub async fn symlink_metadata

(path: P) -> io::Result + async fn symlink_metadata

(path: P) -> io::Result where P: AsRef, { todo!() } - pub async fn trash

(path: P) -> io::Result<()> + async fn trash

(_path: P) -> io::Result<()> where P: AsRef, { - todo!() - } - - pub async fn write(path: P, contents: C) -> io::Result<()> - where - P: AsRef, - C: AsRef<[u8]>, - { - todo!() + Err(io::Error::new(io::ErrorKind::Unsupported, "Trash not supported")) } } + +impl Sftp { + pub(super) async fn op() -> io::Result> { + use deadpool::managed::PoolError; + + super::CONN.get().await.map_err(|e| match e { + PoolError::Timeout(_) => io::Error::new(io::ErrorKind::TimedOut, e.to_string()), + PoolError::Backend(e) => e, + PoolError::Closed | PoolError::NoRuntimeSpecified | PoolError::PostCreateHook(_) => { + io::Error::other(e.to_string()) + } + }) + } +} + +impl deadpool::managed::Manager for Sftp { + type Error = io::Error; + type Type = yazi_sftp::Operator; + + async fn create(&self) -> Result { todo!() } + + async fn recycle( + &self, + obj: &mut Self::Type, + metrics: &deadpool::managed::Metrics, + ) -> deadpool::managed::RecycleResult { + todo!() + } + + fn detach(&self, _obj: &mut Self::Type) { todo!() } +} diff --git a/yazi-fs/src/provider/traits.rs b/yazi-fs/src/provider/traits.rs new file mode 100644 index 00000000..c8816edf --- /dev/null +++ b/yazi-fs/src/provider/traits.rs @@ -0,0 +1,214 @@ +use std::{borrow::Cow, ffi::OsStr, io, path::{Path, PathBuf}}; + +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; +use yazi_macro::ok_or_not_found; + +use crate::cha::Cha; + +pub trait Provider { + type File: AsyncRead + AsyncWrite + Unpin; + type Gate: FileBuilder; + type ReadDir: DirReader; + + fn cache

(_: P) -> Option + where + P: AsRef; + + fn canonicalize

(path: P) -> impl Future> + where + P: AsRef; + + fn copy(from: P, to: Q, cha: Cha) -> impl Future> + where + P: AsRef, + Q: AsRef; + + fn create

(path: P) -> impl Future> + where + P: AsRef, + { + async move { Self::Gate::default().write(true).create(true).truncate(true).open(path).await } + } + + fn create_dir

(path: P) -> impl Future> + where + P: AsRef; + + fn create_dir_all

(path: P) -> impl Future> + where + P: AsRef, + { + async move { + let 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), + } + 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), + } + } + } + + fn hard_link(original: P, link: Q) -> impl Future> + where + P: AsRef, + Q: AsRef; + + fn metadata

(path: P) -> impl Future> + where + P: AsRef; + + fn open

(path: P) -> impl Future> + where + P: AsRef, + { + async move { Self::Gate::default().read(true).open(path).await } + } + + fn read_dir

(path: P) -> impl Future> + where + P: AsRef; + + fn read_link

(path: P) -> impl Future> + where + P: AsRef; + + fn remove_dir

(path: P) -> impl Future> + where + P: AsRef; + + fn remove_dir_all

(path: P) -> impl Future> + where + P: AsRef, + { + async fn remove_dir_all_impl(path: &Path) -> io::Result<()> + where + S: Provider + ?Sized, + { + let mut it = ok_or_not_found!(S::read_dir(path).await, return Ok(())); + while let Some(child) = it.next().await? { + let ft = ok_or_not_found!(child.file_type().await, continue); + let result = if ft.is_dir() { + remove_dir_all_impl::(&child.path()).await + } else { + S::remove_file(&child.path()).await + }; + + () = ok_or_not_found!(result); + } + + Ok(ok_or_not_found!(S::remove_dir(path).await)) + } + + async move { + let path = path.as_ref(); + let ft = ok_or_not_found!(Self::symlink_metadata(path).await, return Ok(())); + if ft.is_symlink() { + Self::remove_file(path).await + } else { + remove_dir_all_impl::(path).await + } + } + } + + fn remove_file

(path: P) -> impl Future> + where + P: AsRef; + + fn rename(from: P, to: Q) -> impl Future> + where + P: AsRef, + Q: AsRef; + + fn symlink(original: P, link: Q, _is_dir: F) -> impl Future> + where + P: AsRef, + Q: AsRef, + F: AsyncFnOnce() -> io::Result; + + fn symlink_dir(original: P, link: Q) -> impl Future> + where + P: AsRef, + Q: AsRef, + { + Self::symlink(original, link, async || Ok(true)) + } + + fn symlink_file(original: P, link: Q) -> impl Future> + where + P: AsRef, + Q: AsRef, + { + Self::symlink(original, link, async || Ok(false)) + } + + fn symlink_metadata

(path: P) -> impl Future> + where + P: AsRef; + + fn trash

(path: P) -> impl Future> + where + P: AsRef; + + fn write(path: P, contents: C) -> impl Future> + where + P: AsRef, + C: AsRef<[u8]>, + { + async move { Self::create(path).await?.write_all(contents.as_ref()).await } + } +} + +// --- DirReader +pub trait DirReader { + type Entry<'a>: FileHolder + where + Self: 'a; + + fn next(&mut self) -> impl Future>>>; +} + +// --- FileHolder +pub trait FileHolder { + fn path(&self) -> PathBuf; + + fn name(&self) -> Cow<'_, OsStr>; + + fn metadata(&self) -> impl Future>; + + fn file_type(&self) -> impl Future>; +} + +// --- FileOpener +pub trait FileBuilder: Default { + type File: AsyncRead + AsyncWrite + Unpin; + + fn append(&mut self, append: bool) -> &mut Self; + + fn create(&mut self, create: bool) -> &mut Self; + + fn create_new(&mut self, create_new: bool) -> &mut Self; + + fn open

(&self, path: P) -> impl Future> + where + P: AsRef; + + fn read(&mut self, read: bool) -> &mut Self; + + fn truncate(&mut self, truncate: bool) -> &mut Self; + + fn write(&mut self, write: bool) -> &mut Self; +} diff --git a/yazi-macro/src/fs.rs b/yazi-macro/src/fs.rs new file mode 100644 index 00000000..a4ab093d --- /dev/null +++ b/yazi-macro/src/fs.rs @@ -0,0 +1,13 @@ +#[macro_export] +macro_rules! ok_or_not_found { + ($result:expr, $not_found:expr) => { + match $result { + Ok(v) => v, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => $not_found, + Err(e) => Err(e)?, + } + }; + ($result:expr) => { + ok_or_not_found!($result, ()) + }; +} diff --git a/yazi-macro/src/lib.rs b/yazi-macro/src/lib.rs index 47918fb1..3a615e16 100644 --- a/yazi-macro/src/lib.rs +++ b/yazi-macro/src/lib.rs @@ -3,6 +3,7 @@ mod asset; mod context; mod event; mod fmt; +mod fs; mod log; mod module; mod platform; diff --git a/yazi-plugin/preset/components/linemode.lua b/yazi-plugin/preset/components/linemode.lua index 676569e0..b69944cb 100644 --- a/yazi-plugin/preset/components/linemode.lua +++ b/yazi-plugin/preset/components/linemode.lua @@ -57,9 +57,9 @@ end function Linemode:permissions() return self._file.cha:perm() or "" end function Linemode:owner() - local user = self._file.cha.uid and ya.user_name(self._file.cha.uid) or self._file.cha.uid - local group = self._file.cha.gid and ya.group_name(self._file.cha.gid) or self._file.cha.gid - return string.format("%s:%s", user or "-", group or "-") + local user = ya.user_name and ya.user_name(self._file.cha.uid) or self._file.cha.uid + local group = ya.group_name and ya.group_name(self._file.cha.gid) or self._file.cha.gid + return string.format("%s:%s", user, group) end function Linemode:redraw() diff --git a/yazi-plugin/src/external/highlighter.rs b/yazi-plugin/src/external/highlighter.rs index 9d3b3f10..b88b45b3 100644 --- a/yazi-plugin/src/external/highlighter.rs +++ b/yazi-plugin/src/external/highlighter.rs @@ -5,7 +5,7 @@ use ratatui::{layout::Size, text::{Line, Span, Text}}; use syntect::{LoadingError, dumps, easy::HighlightLines, highlighting::{self, Theme, ThemeSet}, parsing::{SyntaxReference, SyntaxSet}}; use tokio::io::{AsyncBufReadExt, AsyncSeekExt, BufReader}; use yazi_config::{THEME, YAZI, preview::PreviewWrap}; -use yazi_fs::provider::local::{self, Local}; +use yazi_fs::provider::{Provider, local::Local}; use yazi_shared::{Ids, errors::PeekError, replace_to_printable}; static INCR: Ids = Ids::new(); @@ -132,7 +132,7 @@ impl Highlighter { async fn find_syntax( path: &Path, - reader: &mut BufReader, + reader: &mut BufReader, ) -> Result<&'static SyntaxReference> { let (_, syntaxes) = Self::init(); let name = path.file_name().map(|n| n.to_string_lossy()).unwrap_or_default(); diff --git a/yazi-sftp/src/byte_str.rs b/yazi-sftp/src/byte_str.rs index ac762919..e546ab5e 100644 --- a/yazi-sftp/src/byte_str.rs +++ b/yazi-sftp/src/byte_str.rs @@ -38,6 +38,13 @@ impl<'a> From<&'a Path> for ByteStr<'a> { fn from(value: &'a Path) -> Self { ByteStr::from(value.as_os_str()) } } +impl<'a, T> From<&'a T> for ByteStr<'a> +where + T: AsRef, +{ + fn from(value: &'a T) -> Self { Self::from(value.as_ref()) } +} + impl PartialEq<&str> for ByteStr<'_> { fn eq(&self, other: &&str) -> bool { self.0 == other.as_bytes() } } @@ -94,6 +101,8 @@ impl<'a> ByteStr<'a> { } } + pub fn into_owned(self) -> ByteStr<'static> { ByteStr(Cow::Owned(self.0.into_owned())) } + pub(super) unsafe fn from_str_bytes_unchecked(bytes: &'a [u8]) -> Self { Self(Cow::Borrowed(bytes)) } diff --git a/yazi-sftp/src/fs/file.rs b/yazi-sftp/src/fs/file.rs index b0a79272..bf717c98 100644 --- a/yazi-sftp/src/fs/file.rs +++ b/yazi-sftp/src/fs/file.rs @@ -1,11 +1,11 @@ -use std::{io, pin::Pin, task::{Context, Poll, ready}, time::Duration}; +use std::{io, pin::Pin, sync::Arc, task::{Context, Poll, ready}, time::Duration}; use tokio::{io::{AsyncRead, AsyncWrite, ReadBuf}, sync::oneshot, time::{Timeout, timeout}}; -use crate::{Error, Packet, Session, fs::Attrs}; +use crate::{Error, Operator, Packet, Session, fs::Attrs}; -pub struct File<'a> { - session: &'a Session, +pub struct File { + session: Arc, handle: String, closed: bool, @@ -16,39 +16,41 @@ pub struct File<'a> { flush_rx: Option>>>, } -impl Unpin for File<'_> {} +impl Unpin for File {} -impl Drop for File<'_> { +impl Drop for File { fn drop(&mut self) { if !self.closed { - self.session.close(&self.handle).ok(); + Operator::from(&self.session).close(&self.handle).ok(); } } } -impl<'a> File<'a> { - pub(crate) fn new(session: &'a Session, handle: impl Into) -> Self { +impl File { + pub(crate) fn new(session: &Arc, handle: impl Into) -> Self { Self { - session, - handle: handle.into(), + session: session.clone(), + handle: handle.into(), - closed: false, - cursor: 0, + closed: false, + cursor: 0, close_rx: None, - read_rx: None, + read_rx: None, write_rx: None, flush_rx: None, } } - pub async fn fstat(&self) -> Result { self.session.fstat(&self.handle).await } + pub async fn fstat(&self) -> Result { + Operator::from(&self.session).fstat(&self.handle).await + } pub async fn fsetstat(&self, attrs: Attrs) -> Result<(), Error> { - self.session.fsetstat(&self.handle, attrs).await + Operator::from(&self.session).fsetstat(&self.handle, attrs).await } } -impl AsyncRead for File<'_> { +impl AsyncRead for File { fn poll_read( self: Pin<&mut Self>, cx: &mut Context<'_>, @@ -58,7 +60,7 @@ impl AsyncRead for File<'_> { if me.read_rx.is_none() { let max = buf.remaining().min(261120) as u32; - me.read_rx = Some(me.session.read(&me.handle, me.cursor, max)?); + me.read_rx = Some(Operator::from(&me.session).read(&me.handle, me.cursor, max)?); } let result = ready!(Pin::new(me.read_rx.as_mut().unwrap()).poll(cx)); @@ -79,7 +81,7 @@ impl AsyncRead for File<'_> { } } -impl AsyncWrite for File<'_> { +impl AsyncWrite for File { fn poll_write( self: Pin<&mut Self>, cx: &mut Context<'_>, @@ -91,7 +93,7 @@ impl AsyncWrite for File<'_> { Some((rx, len)) => (rx, *len), None => { let max = buf.len().min(261120); - let rx = me.session.write(&me.handle, me.cursor, &buf[..max])?; + let rx = Operator::from(&me.session).write(&me.handle, me.cursor, &buf[..max])?; (&mut me.write_rx.get_or_insert((rx, max)).0, max) } }; @@ -114,7 +116,7 @@ impl AsyncWrite for File<'_> { let me = unsafe { self.get_unchecked_mut() }; if me.flush_rx.is_none() { - match me.session.fsync(&me.handle) { + match Operator::from(&me.session).fsync(&me.handle) { Ok(rx) => me.flush_rx = Some(timeout(Duration::from_secs(10), rx)), Err(Error::Unsupported) => return Poll::Ready(Ok(())), Err(e) => Err(e)?, @@ -141,7 +143,8 @@ impl AsyncWrite for File<'_> { let me = unsafe { self.get_unchecked_mut() }; if me.close_rx.is_none() { - me.close_rx = Some(timeout(Duration::from_secs(10), me.session.close(&me.handle)?)); + me.close_rx = + Some(timeout(Duration::from_secs(10), Operator::from(&me.session).close(&me.handle)?)); } let rx = unsafe { Pin::new_unchecked(me.close_rx.as_mut().unwrap()) }; diff --git a/yazi-sftp/src/fs/read_dir.rs b/yazi-sftp/src/fs/read_dir.rs index 732b0d5c..bed6e1a3 100644 --- a/yazi-sftp/src/fs/read_dir.rs +++ b/yazi-sftp/src/fs/read_dir.rs @@ -1,20 +1,28 @@ -use std::mem; +use std::{mem, sync::Arc}; use crate::{ByteStr, Error, Session, fs::DirEntry, requests, responses}; -pub struct ReadDir<'a> { - dir: ByteStr<'a>, +pub struct ReadDir { + session: Arc, + dir: ByteStr<'static>, handle: String, - session: &'a Session, - name: responses::Name<'a>, + name: responses::Name<'static>, cursor: usize, done: bool, } -impl<'a> ReadDir<'a> { - pub(crate) fn new(session: &'a Session, dir: ByteStr<'a>, handle: String) -> Self { - Self { dir, handle, session, name: Default::default(), cursor: 0, done: false } +impl ReadDir { + pub(crate) fn new(session: &Arc, dir: ByteStr, handle: String) -> Self { + Self { + session: session.clone(), + dir: dir.into_owned(), + handle, + + name: Default::default(), + cursor: 0, + done: false, + } } pub async fn next(&mut self) -> Result>, Error> { diff --git a/yazi-sftp/src/lib.rs b/yazi-sftp/src/lib.rs index 069c35df..845a865a 100644 --- a/yazi-sftp/src/lib.rs +++ b/yazi-sftp/src/lib.rs @@ -9,6 +9,7 @@ mod de; mod error; mod id; mod macros; +mod operator; mod packet; mod ser; mod session; @@ -17,6 +18,7 @@ pub use byte_str::*; pub(crate) use de::*; pub use error::*; pub(crate) use id::*; +pub use operator::*; pub use packet::*; pub(crate) use ser::*; pub use session::*; diff --git a/yazi-sftp/src/operator.rs b/yazi-sftp/src/operator.rs new file mode 100644 index 00000000..adfbf2bb --- /dev/null +++ b/yazi-sftp/src/operator.rs @@ -0,0 +1,197 @@ +use std::{ops::Deref, path::PathBuf, sync::Arc}; + +use tokio::sync::oneshot; + +use crate::{ByteStr, Error, Packet, Session, fs::{Attrs, File, Flags, ReadDir}, requests, responses}; + +pub struct Operator(Arc); + +impl Deref for Operator { + type Target = Session; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl From<&Arc> for Operator { + fn from(session: &Arc) -> Self { Self(session.clone()) } +} + +impl Operator { + pub async fn init(&mut self) -> Result<(), Error> { + let version: responses::Version = self.send(requests::Init::default()).await?; + *self.extensions.lock() = version.extensions; + Ok(()) + } + + pub async fn open<'a, P>(&self, path: P, flags: Flags, attrs: Attrs) -> Result + where + P: Into>, + { + let handle: responses::Handle = self.send(requests::Open::new(path, flags, attrs)).await?; + + Ok(File::new(&self.0, handle.handle)) + } + + pub fn close(&self, handle: &str) -> Result>, Error> { + self.send_sync(requests::Close::new(handle)) + } + + pub fn read( + &self, + handle: &str, + offset: u64, + len: u32, + ) -> Result>, Error> { + self.send_sync(requests::Read::new(handle, offset, len)) + } + + pub fn write( + &self, + handle: &str, + offset: u64, + data: &[u8], + ) -> Result>, Error> { + self.send_sync(requests::Write::new(handle, offset, data)) + } + + pub async fn lstat<'a, P>(&self, path: P) -> Result + where + P: Into>, + { + let attrs: responses::Attrs = self.send(requests::Lstat::new(path)).await?; + Ok(attrs.attrs) + } + + pub async fn fstat(&self, handle: &str) -> Result { + let attrs: responses::Attrs = self.send(requests::Fstat::new(handle)).await?; + Ok(attrs.attrs) + } + + pub async fn setstat<'a, P>(&self, path: P, attrs: Attrs) -> Result<(), Error> + where + P: Into>, + { + let status: responses::Status = self.send(requests::SetStat::new(path, attrs)).await?; + status.into() + } + + pub async fn fsetstat(&self, handle: &str, attrs: Attrs) -> Result<(), Error> { + let status: responses::Status = self.send(requests::FSetStat::new(handle, attrs)).await?; + status.into() + } + + pub async fn read_dir<'a>(&'a self, dir: impl Into>) -> Result { + let dir: ByteStr = dir.into(); + let handle: responses::Handle = self.send(requests::OpenDir::new(&dir)).await?; + + Ok(ReadDir::new(&self.0, dir, handle.handle)) + } + + pub async fn remove<'a, P>(&self, path: P) -> Result<(), Error> + where + P: Into>, + { + let status: responses::Status = self.send(requests::Remove::new(path)).await?; + status.into() + } + + pub async fn mkdir<'a, P>(&self, path: P, attrs: Attrs) -> Result<(), Error> + where + P: Into>, + { + let status: responses::Status = self.send(requests::Mkdir::new(path, attrs)).await?; + status.into() + } + + pub async fn rmdir<'a, P>(&self, path: P) -> Result<(), Error> + where + P: Into>, + { + let status: responses::Status = self.send(requests::Rmdir::new(path)).await?; + status.into() + } + + pub async fn realpath<'a, P>(&self, path: P) -> Result + where + P: Into>, + { + let mut name: responses::Name = self.send(requests::Realpath::new(path)).await?; + if name.items.is_empty() { + Err(Error::custom("realpath returned no names")) + } else { + Ok(name.items.swap_remove(0).name.into_path()) + } + } + + pub async fn stat<'a, P>(&self, path: P) -> Result + where + P: Into>, + { + let attrs: responses::Attrs = self.send(requests::Stat::new(path)).await?; + Ok(attrs.attrs) + } + + pub async fn rename<'a, F, T>(&self, from: F, to: T) -> Result<(), Error> + where + F: Into>, + T: Into>, + { + let status: responses::Status = self.send(requests::Rename::new(from, to)).await?; + status.into() + } + + pub async fn readlink<'a, P>(&self, path: P) -> Result + where + P: Into>, + { + let mut name: responses::Name = self.send(requests::Readlink::new(path)).await?; + if name.items.is_empty() { + Err(Error::custom("readlink returned no names")) + } else { + Ok(name.items.swap_remove(0).name.into_path()) + } + } + + pub async fn symlink<'a, L, O>(&self, original: O, link: L) -> Result<(), Error> + where + O: Into>, + L: Into>, + { + let status: responses::Status = self.send(requests::Symlink::new(original, link)).await?; + status.into() + } + + pub fn fsync(&self, handle: &str) -> Result>, Error> { + if self.extensions.lock().get("fsync@openssh.com").is_none_or(|s| s != "1") { + return Err(Error::Unsupported); + } + + let data = requests::ExtendedFsync::new(handle); + self.send_sync(requests::Extended::new("fsync@openssh.com", data)) + } + + pub async fn hardlink<'a, O, L>(&self, original: O, link: L) -> Result<(), Error> + where + O: Into>, + L: Into>, + { + if self.extensions.lock().get("hardlink@openssh.com").is_none_or(|s| s != "1") { + return Err(Error::Unsupported); + } + + let data = requests::ExtendedHardlink::new(original, link); + let status: responses::Status = + self.send(requests::Extended::new("hardlink@openssh.com", data)).await?; + status.into() + } + + pub async fn limits(&self) -> Result { + if self.extensions.lock().get("limits@openssh.com").is_none_or(|s| s != "1") { + return Err(Error::Unsupported); + } + + let extended: responses::Extended = + self.send(requests::Extended::new("limits@openssh.com", requests::ExtendedLimits)).await?; + extended.try_into() + } +} diff --git a/yazi-sftp/src/session.rs b/yazi-sftp/src/session.rs index 7ad34583..4e14d683 100644 --- a/yazi-sftp/src/session.rs +++ b/yazi-sftp/src/session.rs @@ -1,25 +1,30 @@ -use std::{any::TypeId, collections::HashMap, io::ErrorKind, path::PathBuf, sync::Arc}; +use std::{any::TypeId, collections::HashMap, io::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 crate::{ByteStr, Error, Id, Packet, fs::{Attrs, File, Flags, ReadDir}, requests, responses}; +use crate::{Error, Id, Packet, responses}; pub struct Session { - tx: mpsc::UnboundedSender>, - pub(crate) id: Id, - callback: Arc>>>>, - extensions: HashMap, + tx: mpsc::UnboundedSender>, + id: Id, + callback: Mutex>>>, + pub(super) extensions: Mutex>, } impl Session { - pub fn make(stream: ChannelStream) -> Self { - let (tx, mut rx) = mpsc::unbounded_channel::>(); - let (mut reader, mut writer) = tokio::io::split(stream); - let callback = Arc::new(Mutex::new(HashMap::<_, oneshot::Sender<_>>::new())); + pub fn make(stream: ChannelStream) -> Arc { + let (tx, mut rx) = mpsc::unbounded_channel(); + let me = Arc::new(Self { + tx, + id: Id::default(), + callback: Default::default(), + extensions: Default::default(), + }); + let (mut reader, mut writer) = tokio::io::split(stream); tokio::spawn(async move { while let Some(data) = rx.recv().await { if data.is_empty() { @@ -32,206 +37,28 @@ impl Session { } }); - let (tx_, callback_) = (tx.clone(), callback.clone()); + let me_ = me.clone(); 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 { - tx_.send(vec![]).ok(); + me_.tx.send(vec![]).ok(); break; } if let Ok(packet) = crate::from_bytes(&buf) - && let Some(cb) = callback_.lock().remove(&packet.id()) + && let Some(cb) = me_.callback.lock().remove(&packet.id()) { cb.send(packet).ok(); } } - _ = tx_.closed() => break, + _ = me_.tx.closed() => break, } } }); - Self { tx, id: Id::default(), callback, extensions: HashMap::new() } - } - - pub async fn init(&mut self) -> Result<(), Error> { - let version: responses::Version = self.send(requests::Init::default()).await?; - self.extensions = version.extensions; - Ok(()) - } - - pub async fn open<'a, P>(&self, path: P, flags: Flags, attrs: Attrs) -> Result, Error> - where - P: Into>, - { - let handle: responses::Handle = self.send(requests::Open::new(path, flags, attrs)).await?; - - Ok(File::new(self, handle.handle)) - } - - pub fn close(&self, handle: &str) -> Result>, Error> { - self.send_sync(requests::Close::new(handle)) - } - - pub fn read( - &self, - handle: &str, - offset: u64, - len: u32, - ) -> Result>, Error> { - self.send_sync(requests::Read::new(handle, offset, len)) - } - - pub fn write( - &self, - handle: &str, - offset: u64, - data: &[u8], - ) -> Result>, Error> { - self.send_sync(requests::Write::new(handle, offset, data)) - } - - pub async fn lstat<'a, P>(&self, path: P) -> Result - where - P: Into>, - { - let attrs: responses::Attrs = self.send(requests::Lstat::new(path)).await?; - Ok(attrs.attrs) - } - - pub async fn fstat(&self, handle: &str) -> Result { - let attrs: responses::Attrs = self.send(requests::Fstat::new(handle)).await?; - Ok(attrs.attrs) - } - - pub async fn setstat<'a, P>(&self, path: P, attrs: Attrs) -> Result<(), Error> - where - P: Into>, - { - let status: responses::Status = self.send(requests::SetStat::new(path, attrs)).await?; - status.into() - } - - pub async fn fsetstat(&self, handle: &str, attrs: Attrs) -> Result<(), Error> { - let status: responses::Status = self.send(requests::FSetStat::new(handle, attrs)).await?; - status.into() - } - - pub async fn read_dir<'a>(&'a self, dir: impl Into>) -> Result, Error> { - let dir: ByteStr = dir.into(); - let handle: responses::Handle = self.send(requests::OpenDir::new(&dir)).await?; - - Ok(ReadDir::new(self, dir, handle.handle)) - } - - pub async fn remove<'a, P>(&self, path: P) -> Result<(), Error> - where - P: Into>, - { - let status: responses::Status = self.send(requests::Remove::new(path)).await?; - status.into() - } - - pub async fn mkdir<'a, P>(&self, path: P, attrs: Attrs) -> Result<(), Error> - where - P: Into>, - { - let status: responses::Status = self.send(requests::Mkdir::new(path, attrs)).await?; - status.into() - } - - pub async fn rmdir<'a, P>(&self, path: P) -> Result<(), Error> - where - P: Into>, - { - let status: responses::Status = self.send(requests::Rmdir::new(path)).await?; - status.into() - } - - pub async fn realpath<'a, P>(&self, path: P) -> Result - where - P: Into>, - { - let mut name: responses::Name = self.send(requests::Realpath::new(path)).await?; - if name.items.is_empty() { - Err(Error::custom("realpath returned no names")) - } else { - Ok(name.items.swap_remove(0).name.into_path()) - } - } - - pub async fn stat<'a, P>(&self, path: P) -> Result - where - P: Into>, - { - let attrs: responses::Attrs = self.send(requests::Stat::new(path)).await?; - Ok(attrs.attrs) - } - - pub async fn rename<'a, F, T>(&self, from: F, to: T) -> Result<(), Error> - where - F: Into>, - T: Into>, - { - let status: responses::Status = self.send(requests::Rename::new(from, to)).await?; - status.into() - } - - pub async fn readlink<'a, P>(&self, path: P) -> Result - where - P: Into>, - { - let mut name: responses::Name = self.send(requests::Readlink::new(path)).await?; - if name.items.is_empty() { - Err(Error::custom("readlink returned no names")) - } else { - Ok(name.items.swap_remove(0).name.into_path()) - } - } - - pub async fn symlink<'a, L, O>(&self, link: L, original: O) -> Result<(), Error> - where - L: Into>, - O: Into>, - { - let status: responses::Status = self.send(requests::Symlink::new(link, original)).await?; - status.into() - } - - pub fn fsync(&self, handle: &str) -> Result>, Error> { - if self.extensions.get("fsync@openssh.com").is_none_or(|s| s != "1") { - return Err(Error::Unsupported); - } - - let data = requests::ExtendedFsync::new(handle); - self.send_sync(requests::Extended::new("fsync@openssh.com", data)) - } - - pub async fn hardlink<'a, O, L>(&self, original: O, link: L) -> Result<(), Error> - where - O: Into>, - L: Into>, - { - if self.extensions.get("hardlink@openssh.com").is_none_or(|s| s != "1") { - return Err(Error::Unsupported); - } - - let data = requests::ExtendedHardlink::new(original, link); - let status: responses::Status = - self.send(requests::Extended::new("hardlink@openssh.com", data)).await?; - status.into() - } - - pub async fn limits(&self) -> Result { - if self.extensions.get("limits@openssh.com").is_none_or(|s| s != "1") { - return Err(Error::Unsupported); - } - - let extended: responses::Extended = - self.send(requests::Extended::new("limits@openssh.com", requests::ExtendedLimits)).await?; - extended.try_into() + me } pub async fn send<'a, I, O>(&self, input: I) -> Result diff --git a/yazi-shared/src/url/cow.rs b/yazi-shared/src/url/cow.rs index 6f7def57..b09154fe 100644 --- a/yazi-shared/src/url/cow.rs +++ b/yazi-shared/src/url/cow.rs @@ -53,9 +53,7 @@ impl<'a> TryFrom<&'a [u8]> for UrlCow<'a> { Self::Borrowed { loc: Loc::with(p, uri, urn)?, scheme } } (Cow::Owned(p), None) => Self::Owned { loc: LocBuf::from(p), scheme }, - (Cow::Owned(p), Some((uri, urn))) => { - Self::Owned { loc: LocBuf::with(p, uri, urn)?, scheme } - } + (Cow::Owned(p), Some((uri, urn))) => Self::Owned { loc: LocBuf::with(p, uri, urn)?, scheme }, }) } } diff --git a/yazi-watcher/src/backend/backend.rs b/yazi-watcher/src/backend/backend.rs index 5a69b305..8fa23fdf 100644 --- a/yazi-watcher/src/backend/backend.rs +++ b/yazi-watcher/src/backend/backend.rs @@ -13,7 +13,7 @@ pub(crate) struct Backend { impl Backend { pub(crate) fn serve(out_tx: mpsc::UnboundedSender) -> Self { #[cfg(any(target_os = "linux", target_os = "macos"))] - yazi_fs::mounts::Partitions::monitor(yazi_fs::mounts::PARTITIONS.clone(), || { + yazi_fs::mounts::Partitions::monitor(&yazi_fs::mounts::PARTITIONS, || { yazi_macro::err!(yazi_dds::Pubsub::pub_after_mount()) });