feat: SFTP virtual filesystem provider (#3172)

This commit is contained in:
三咲雅 misaki masa 2025-09-17 08:34:35 +08:00 committed by GitHub
parent 7deeaa4356
commit 6cfa92f112
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 1086 additions and 749 deletions

39
Cargo.lock generated
View file

@ -874,6 +874,27 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 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]] [[package]]
name = "delegate" name = "delegate"
version = "0.13.4" version = "0.13.4"
@ -1483,6 +1504,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]] [[package]]
name = "hex" name = "hex"
version = "0.4.3" version = "0.4.3"
@ -2185,6 +2212,16 @@ dependencies = [
"libm", "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]] [[package]]
name = "num_threads" name = "num_threads"
version = "0.1.7" version = "0.1.7"
@ -4810,6 +4847,7 @@ dependencies = [
"arc-swap", "arc-swap",
"bitflags 2.9.4", "bitflags 2.9.4",
"core-foundation-sys", "core-foundation-sys",
"deadpool",
"dirs", "dirs",
"foldhash 0.2.0", "foldhash 0.2.0",
"futures", "futures",
@ -4827,6 +4865,7 @@ dependencies = [
"windows-sys 0.61.0", "windows-sys 0.61.0",
"yazi-ffi", "yazi-ffi",
"yazi-macro", "yazi-macro",
"yazi-sftp",
"yazi-shared", "yazi-shared",
] ]

View file

@ -47,7 +47,7 @@ impl Actor for Trigger {
while let Ok(Some(ent)) = dir.next_entry().await { while let Ok(Some(ent)) = dir.next_entry().await {
if let Ok(ft) = ent.file_type().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() });
} }
} }

View file

@ -7,7 +7,7 @@ use scopeguard::defer;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use yazi_config::{YAZI, opener::OpenerRule}; use yazi_config::{YAZI, opener::OpenerRule};
use yazi_dds::Pubsub; 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_macro::{err, succ};
use yazi_parser::VoidOpt; use yazi_parser::VoidOpt;
use yazi_proxy::{AppProxy, HIDER, TasksProxy}; use yazi_proxy::{AppProxy, HIDER, TasksProxy};

View file

@ -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 image::{DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation};
use ratatui::layout::Rect; use ratatui::layout::Rect;
use yazi_config::YAZI; use yazi_config::YAZI;
use yazi_fs::provider::local::Local; use yazi_fs::provider::{Provider, local::Local};
use crate::Dimension; use crate::Dimension;

View file

@ -37,16 +37,11 @@ impl Cha {
len: t.raw_get("len").unwrap_or_default(), len: t.raw_get("len").unwrap_or_default(),
atime: parse_time(t.raw_get("atime").ok())?, atime: parse_time(t.raw_get("atime").ok())?,
btime: parse_time(t.raw_get("btime").ok())?, btime: parse_time(t.raw_get("btime").ok())?,
#[cfg(unix)]
ctime: parse_time(t.raw_get("ctime").ok())?, ctime: parse_time(t.raw_get("ctime").ok())?,
mtime: parse_time(t.raw_get("mtime").ok())?, mtime: parse_time(t.raw_get("mtime").ok())?,
#[cfg(unix)]
dev: t.raw_get("dev").unwrap_or_default(), dev: t.raw_get("dev").unwrap_or_default(),
#[cfg(unix)]
uid: t.raw_get("uid").unwrap_or_default(), uid: t.raw_get("uid").unwrap_or_default(),
#[cfg(unix)]
gid: t.raw_get("gid").unwrap_or_default(), gid: t.raw_get("gid").unwrap_or_default(),
#[cfg(unix)]
nlink: t.raw_get("nlink").unwrap_or_default(), nlink: t.raw_get("nlink").unwrap_or_default(),
}) })
.into_lua(lua) .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_exec", |_, me| Ok(me.is_exec()));
fields.add_field_method_get("is_sticky", |_, me| Ok(me.is_sticky())); 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("len", |_, me| Ok(me.len));
fields.add_field_method_get("atime", |_, me| { 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())) 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| { 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())) 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| { 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())) 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| { 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())) 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<M: UserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {

View file

@ -1,5 +1,5 @@
use anyhow::{Context, Result}; 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 yazi_macro::outln;
use super::Dependency; use super::Dependency;
@ -25,7 +25,7 @@ impl Dependency {
let assets = self.target().join("assets"); let assets = self.target().join("assets");
match Local::read_dir(&assets).await { match Local::read_dir(&assets).await {
Ok(mut it) => { Ok(mut it) => {
while let Some(entry) = it.next_entry().await? { while let Some(entry) = it.next().await? {
remove_sealed(&entry.path()) remove_sealed(&entry.path())
.await .await
.with_context(|| format!("failed to remove `{}`", entry.path().display()))?; .with_context(|| format!("failed to remove `{}`", entry.path().display()))?;

View file

@ -3,7 +3,7 @@ use std::{io::BufWriter, path::{Path, PathBuf}, str::FromStr};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use twox_hash::XxHash3_128; 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; use yazi_shared::BytesExt;
#[derive(Clone, Default)] #[derive(Clone, Default)]
@ -65,8 +65,8 @@ impl Dependency {
let mut it = Local::read_dir(dir).await?; let mut it = Local::read_dir(dir).await?;
let mut files: Vec<String> = let mut files: Vec<String> =
["LICENSE", "README.md", "main.lua"].into_iter().map(Into::into).collect(); ["LICENSE", "README.md", "main.lua"].into_iter().map(Into::into).collect();
while let Some(entry) = it.next_entry().await? { while let Some(entry) = it.next().await? {
if let Ok(name) = entry.file_name().into_string() if let Ok(name) = entry.name().into_owned().into_string()
&& let Some(stripped) = name.strip_suffix(".lua") && let Some(stripped) = name.strip_suffix(".lua")
&& stripped != "main" && stripped != "main"
&& stripped.as_bytes().kebab_cased() && stripped.as_bytes().kebab_cased()

View file

@ -1,7 +1,7 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::{Context, Result}; 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 yazi_macro::outln;
use super::Dependency; use super::Dependency;
@ -43,8 +43,8 @@ impl Dependency {
match Local::read_dir(&from).await { match Local::read_dir(&from).await {
Ok(mut it) => { Ok(mut it) => {
Local::create_dir_all(&to).await?; Local::create_dir_all(&to).await?;
while let Some(entry) = it.next_entry().await? { while let Some(entry) = it.next().await? {
let (src, dist) = (entry.path(), to.join(entry.file_name())); let (src, dist) = (entry.path(), to.join(entry.name()));
copy_and_seal(&src, &dist).await.with_context(|| { copy_and_seal(&src, &dist).await.with_context(|| {
format!("failed to copy `{}` to `{}`", src.display(), dist.display()) format!("failed to copy `{}` to `{}`", src.display(), dist.display())
})?; })?;

View file

@ -1,6 +1,6 @@
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use twox_hash::XxHash3_128; 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; use super::Dependency;
@ -20,8 +20,8 @@ impl Dependency {
let mut assets = vec![]; let mut assets = vec![];
match Local::read_dir(dir.join("assets")).await { match Local::read_dir(dir.join("assets")).await {
Ok(mut it) => { Ok(mut it) => {
while let Some(entry) = it.next_entry().await? { while let Some(entry) = it.next().await? {
let Ok(name) = entry.file_name().into_string() else { let Ok(name) = entry.name().into_owned().into_string() else {
bail!("asset path is not valid UTF-8: {}", entry.path().display()); bail!("asset path is not valid UTF-8: {}", entry.path().display());
}; };
assets.push((name, Local::read(entry.path()).await?)); assets.push((name, Local::read(entry.path()).await?));

View file

@ -2,7 +2,7 @@ use std::{path::PathBuf, str::FromStr};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use serde::{Deserialize, Deserializer, Serialize, Serializer}; 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 yazi_macro::outln;
use super::Dependency; use super::Dependency;

View file

@ -1,7 +1,7 @@
use std::{io, path::Path}; use std::{io, path::Path};
use tokio::io::AsyncWriteExt; 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] #[inline]
pub async fn must_exists(path: impl AsRef<Path>) -> bool { pub async fn must_exists(path: impl AsRef<Path>) -> bool {

View file

@ -5,7 +5,7 @@ use hashbrown::HashMap;
use parking_lot::RwLock; use parking_lot::RwLock;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
use yazi_boot::BOOT; 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 yazi_shared::{RoCell, timestamp_us};
use crate::CLIENTS; use crate::CLIENTS;

View file

@ -36,6 +36,8 @@ impl Stream {
#[cfg(unix)] #[cfg(unix)]
pub(super) async fn bind() -> std::io::Result<ServerListener> { pub(super) async fn bind() -> std::io::Result<ServerListener> {
use yazi_fs::provider::Provider;
let p = Self::socket_file(); let p = Self::socket_file();
yazi_fs::provider::local::Local::remove_file(&p).await.ok(); yazi_fs::provider::local::Local::remove_file(&p).await.ok();

View file

@ -1,7 +1,7 @@
use std::ffi::OsString; use std::ffi::OsString;
use yazi_boot::ARGS; use yazi_boot::ARGS;
use yazi_fs::provider::local::Local; use yazi_fs::provider::{Provider, local::Local};
use yazi_shared::event::EventQuit; use yazi_shared::event::EventQuit;
use crate::{Term, app::App}; use crate::{Term, app::App};

View file

@ -11,12 +11,14 @@ repository = "https://github.com/sxyazi/yazi"
[dependencies] [dependencies]
yazi-ffi = { path = "../yazi-ffi", version = "25.9.15" } yazi-ffi = { path = "../yazi-ffi", version = "25.9.15" }
yazi-macro = { path = "../yazi-macro", version = "25.9.15" } yazi-macro = { path = "../yazi-macro", version = "25.9.15" }
yazi-sftp = { path = "../yazi-sftp", version = "0.1.0" }
yazi-shared = { path = "../yazi-shared", version = "25.9.15" } yazi-shared = { path = "../yazi-shared", version = "25.9.15" }
# External dependencies # External dependencies
anyhow = { workspace = true } anyhow = { workspace = true }
arc-swap = "1.7.1" arc-swap = "1.7.1"
bitflags = { workspace = true } bitflags = { workspace = true }
deadpool = { version = "0.12.3", default-features = false, features = [ "managed", "rt_tokio_1" ] }
dirs = { workspace = true } dirs = { workspace = true }
foldhash = { workspace = true } foldhash = { workspace = true }
futures = { workspace = true } futures = { workspace = true }

View file

@ -13,16 +13,11 @@ pub struct Cha {
pub len: u64, pub len: u64,
pub atime: Option<SystemTime>, pub atime: Option<SystemTime>,
pub btime: Option<SystemTime>, pub btime: Option<SystemTime>,
#[cfg(unix)]
pub ctime: Option<SystemTime>, pub ctime: Option<SystemTime>,
pub mtime: Option<SystemTime>, pub mtime: Option<SystemTime>,
#[cfg(unix)] pub dev: u64,
pub dev: libc::dev_t,
#[cfg(unix)]
pub uid: u32, pub uid: u32,
#[cfg(unix)]
pub gid: u32, pub gid: u32,
#[cfg(unix)]
pub nlink: u64, pub nlink: u64,
} }
@ -35,22 +30,17 @@ impl Deref for Cha {
impl Default for Cha { impl Default for Cha {
fn default() -> Self { fn default() -> Self {
Self { Self {
kind: ChaKind::DUMMY, kind: ChaKind::DUMMY,
mode: ChaMode::empty(), mode: ChaMode::empty(),
len: 0, len: 0,
atime: None, atime: None,
btime: None, btime: None,
#[cfg(unix)] ctime: None,
ctime: None, mtime: None,
mtime: None, dev: 0,
#[cfg(unix)] uid: 0,
dev: 0, gid: 0,
#[cfg(unix)] nlink: 0,
uid: 0,
#[cfg(unix)]
gid: 0,
#[cfg(unix)]
nlink: 0,
} }
} }
} }
@ -136,17 +126,15 @@ impl Cha {
len: m.len(), len: m.len(),
atime: m.accessed().ok(), atime: m.accessed().ok(),
btime: m.created().ok(), btime: m.created().ok(),
#[cfg(unix)] ctime: unix_either!(
ctime: UNIX_EPOCH.checked_add(Duration::new(m.ctime() as u64, m.ctime_nsec() as u32)), UNIX_EPOCH.checked_add(Duration::new(m.ctime() as u64, m.ctime_nsec() as u32)),
None
),
mtime: m.modified().ok(), mtime: m.modified().ok(),
#[cfg(unix)] dev: unix_either!(m.dev(), 0) as _,
dev: m.dev() as _, uid: unix_either!(m.uid(), 0) as _,
#[cfg(unix)] gid: unix_either!(m.gid(), 0) as _,
uid: m.uid() as _, nlink: unix_either!(m.nlink(), 0) as _,
#[cfg(unix)]
gid: m.gid() as _,
#[cfg(unix)]
nlink: m.nlink() as _,
} }
} }

View file

@ -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); yazi_macro::mod_pub!(cha mounts provider path);
@ -8,4 +8,6 @@ pub fn init() {
CWD.init(<_>::default()); CWD.init(<_>::default());
mounts::init(); mounts::init();
provider::init();
} }

View file

@ -9,22 +9,22 @@ use yazi_shared::{natsort, replace_cow, replace_vec_cow};
use super::{Locked, Partition, Partitions}; use super::{Locked, Partition, Partitions};
impl Partitions { impl Partitions {
pub fn monitor<F>(me: Locked, cb: F) pub fn monitor<F>(me: &'static Locked, cb: F)
where where
F: Fn() + Copy + Send + 'static, 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 f = std::fs::File::open("/proc/mounts")?;
let fd = AsyncFd::with_interest(f.as_fd(), Interest::READABLE)?; let fd = AsyncFd::with_interest(f.as_fd(), Interest::READABLE)?;
loop { loop {
let mut guard = fd.readable().await?; let mut guard = fd.readable().await?;
guard.clear_ready(); guard.clear_ready();
Partitions::update(me.clone()).await; Partitions::update(me).await;
cb(); cb();
} }
} }
async fn wait_partitions(me: Locked, cb: impl Fn()) -> Result<()> { async fn wait_partitions(me: &'static Locked, cb: impl Fn()) -> Result<()> {
loop { loop {
let partitions = Partitions::partitions()?; let partitions = Partitions::partitions()?;
if me.read().linux_cache == partitions { if me.read().linux_cache == partitions {
@ -33,17 +33,16 @@ impl Partitions {
} }
me.write().linux_cache = partitions; me.write().linux_cache = partitions;
Partitions::update(me.clone()).await; Partitions::update(me).await;
cb(); cb();
sleep(Duration::from_secs(3)).await; sleep(Duration::from_secs(3)).await;
} }
} }
let me_ = me.clone();
tokio::spawn(async move { tokio::spawn(async move {
loop { 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:?}"); error!("Error encountered while monitoring /proc/mounts: {e:?}");
} }
sleep(Duration::from_secs(5)).await; sleep(Duration::from_secs(5)).await;
@ -52,7 +51,7 @@ impl Partitions {
tokio::spawn(async move { tokio::spawn(async move {
loop { 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:?}"); error!("Error encountered while monitoring /proc/partitions: {e:?}");
} }
sleep(Duration::from_secs(5)).await; sleep(Duration::from_secs(5)).await;
@ -75,7 +74,7 @@ impl Partitions {
Ok(set) Ok(set)
} }
async fn update(me: Locked) { async fn update(me: &'static Locked) {
_ = tokio::task::spawn_blocking(move || { _ = tokio::task::spawn_blocking(move || {
let mut guard = me.write(); let mut guard = me.write();
match Self::all(&guard) { match Self::all(&guard) {

View file

@ -12,7 +12,7 @@ use yazi_shared::natsort;
use super::{Locked, Partition, Partitions}; use super::{Locked, Partition, Partitions};
impl Partitions { impl Partitions {
pub fn monitor<F>(me: Locked, cb: F) pub fn monitor<F>(me: &'static Locked, cb: F)
where where
F: Fn() + Copy + Send + 'static, F: Fn() + Copy + Send + 'static,
{ {
@ -39,12 +39,11 @@ impl Partitions {
} }
let create_context = || { let create_context = || {
let me = me.clone();
let boxed: Box<dyn Fn()> = Box::new(move || { let boxed: Box<dyn Fn()> = Box::new(move || {
if mem::replace(&mut me.write().need_update, true) { if mem::replace(&mut me.write().need_update, true) {
return; return;
} }
Self::update(me.clone(), cb); Self::update(me, cb);
}); });
Box::into_raw(Box::new(boxed)) as *mut c_void 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 || { _ = tokio::task::spawn_blocking(move || {
let result = Self::all_names().and_then(Self::all_partitions); let result = Self::all_names().and_then(Self::all_partitions);
if let Err(ref e) = result { if let Err(ref e) = result {

View file

@ -5,7 +5,7 @@ pub struct Partition {
pub src: OsString, pub src: OsString,
pub dist: Option<PathBuf>, pub dist: Option<PathBuf>,
#[cfg(unix)] #[cfg(unix)]
pub rdev: Option<libc::dev_t>, pub rdev: Option<u64>,
pub label: Option<OsString>, pub label: Option<OsString>,
pub fstype: Option<OsString>, pub fstype: Option<OsString>,
pub capacity: u64, pub capacity: u64,

View file

@ -1,4 +1,4 @@
use std::{ops::Deref, sync::Arc}; use std::ops::Deref;
use parking_lot::RwLock; use parking_lot::RwLock;
use yazi_shared::RoCell; use yazi_shared::RoCell;
@ -6,7 +6,7 @@ use yazi_shared::RoCell;
use super::Partition; use super::Partition;
use crate::cha::Cha; use crate::cha::Cha;
pub(super) type Locked = Arc<RwLock<Partitions>>; pub(super) type Locked = RwLock<Partitions>;
pub static PARTITIONS: RoCell<Locked> = RoCell::new(); pub static PARTITIONS: RoCell<Locked> = RoCell::new();
@ -27,7 +27,7 @@ impl Deref for Partitions {
impl Partitions { impl Partitions {
#[cfg(unix)] #[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)) self.inner.iter().find(|p| p.rdev == Some(dev))
} }

View file

@ -1,23 +1,29 @@
use std::{ffi::OsString, io}; use std::{borrow::Cow, ffi::OsStr, io};
use yazi_shared::url::UrlBuf; use yazi_shared::url::UrlBuf;
use crate::provider::FileHolder;
pub enum DirEntry { pub enum DirEntry {
Local(super::local::DirEntry), Local(super::local::DirEntry),
} }
impl From<super::local::DirEntry> for DirEntry {
fn from(value: super::local::DirEntry) -> Self { Self::Local(value) }
}
impl DirEntry { impl DirEntry {
#[must_use] #[must_use]
pub fn url(&self) -> UrlBuf { pub fn url(&self) -> UrlBuf {
match self { match self {
Self::Local(local) => local.url(), Self::Local(local) => local.path().into(),
} }
} }
#[must_use] #[must_use]
pub fn file_name(&self) -> OsString { pub fn name(&self) -> Cow<'_, OsStr> {
match self { match self {
Self::Local(local) => local.file_name(), Self::Local(local) => local.name(),
} }
} }

View file

@ -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 { impl FileHolder for DirEntry {
type Target = tokio::fs::DirEntry; fn path(&self) -> PathBuf { self.0.path() }
fn deref(&self) -> &Self::Target { &self.0 } fn name(&self) -> Cow<'_, OsStr> { self.0.file_name().into() }
}
async fn metadata(&self) -> io::Result<std::fs::Metadata> { self.0.metadata().await }
impl From<tokio::fs::DirEntry> for DirEntry {
fn from(value: tokio::fs::DirEntry) -> Self { Self(value) } async fn file_type(&self) -> io::Result<std::fs::FileType> { self.0.file_type().await }
}
impl From<DirEntry> 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() }
} }

View file

@ -1,14 +1,47 @@
use std::ops::{Deref, DerefMut}; use std::{io, path::Path};
use crate::provider::FileBuilder;
#[derive(Default)] #[derive(Default)]
pub struct Gate(tokio::fs::OpenOptions); pub struct Gate(tokio::fs::OpenOptions);
impl Deref for Gate { impl FileBuilder for Gate {
type Target = tokio::fs::OpenOptions; 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 create(&mut self, create: bool) -> &mut Self {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } 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<P>(&self, path: P) -> io::Result<Self::File>
where
P: AsRef<Path>,
{
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
}
} }

View file

@ -1,12 +1,16 @@
use std::{io, path::{Path, PathBuf}}; use std::{io, path::{Path, PathBuf}};
use crate::{cha::Cha, provider::local::{Gate, ReadDir, RwFile}}; use crate::{cha::Cha, provider::Provider};
pub struct Local; pub struct Local;
impl Local { impl Provider for Local {
type File = tokio::fs::File;
type Gate = super::Gate;
type ReadDir = super::ReadDir;
#[inline] #[inline]
pub fn cache<P>(_: P) -> Option<PathBuf> fn cache<P>(_: P) -> Option<PathBuf>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -14,7 +18,7 @@ impl Local {
} }
#[inline] #[inline]
pub async fn canonicalize<P>(path: P) -> io::Result<PathBuf> async fn canonicalize<P>(path: P) -> io::Result<PathBuf>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -22,7 +26,7 @@ impl Local {
} }
#[inline] #[inline]
pub async fn copy<P, Q>(from: P, to: Q, cha: Cha) -> io::Result<u64> async fn copy<P, Q>(from: P, to: Q, cha: Cha) -> io::Result<u64>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
@ -32,6 +36,183 @@ impl Local {
Self::copy_impl(from, to, cha).await Self::copy_impl(from, to, cha).await
} }
#[inline]
async fn create_dir<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
tokio::fs::create_dir(path).await
}
#[inline]
async fn create_dir_all<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
tokio::fs::create_dir_all(path).await
}
#[inline]
async fn hard_link<P, Q>(original: P, link: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
tokio::fs::hard_link(original, link).await
}
#[inline]
async fn metadata<P>(path: P) -> io::Result<std::fs::Metadata>
where
P: AsRef<Path>,
{
tokio::fs::metadata(path).await
}
#[inline]
async fn read_dir<P>(path: P) -> io::Result<Self::ReadDir>
where
P: AsRef<Path>,
{
tokio::fs::read_dir(path).await.map(super::ReadDir)
}
#[inline]
async fn read_link<P>(path: P) -> io::Result<PathBuf>
where
P: AsRef<Path>,
{
tokio::fs::read_link(path).await
}
#[inline]
async fn remove_dir<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
tokio::fs::remove_dir(path).await
}
#[inline]
async fn remove_dir_all<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
tokio::fs::remove_dir_all(path).await
}
#[inline]
async fn remove_file<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
tokio::fs::remove_file(path).await
}
#[inline]
async fn rename<P, Q>(from: P, to: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
tokio::fs::rename(from, to).await
}
#[inline]
async fn symlink<P, Q, F>(original: P, link: Q, _is_dir: F) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
F: AsyncFnOnce() -> io::Result<bool>,
{
#[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<P, Q>(original: P, link: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
#[cfg(unix)]
{
tokio::fs::symlink(original, link).await
}
#[cfg(windows)]
{
tokio::fs::symlink_dir(original, link).await
}
}
#[inline]
async fn symlink_file<P, Q>(original: P, link: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
#[cfg(unix)]
{
tokio::fs::symlink(original, link).await
}
#[cfg(windows)]
{
tokio::fs::symlink_file(original, link).await
}
}
#[inline]
async fn symlink_metadata<P>(path: P) -> io::Result<std::fs::Metadata>
where
P: AsRef<Path>,
{
tokio::fs::symlink_metadata(path).await
}
async fn trash<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
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<P, C>(path: P, contents: C) -> io::Result<()>
where
P: AsRef<Path>,
C: AsRef<[u8]>,
{
tokio::fs::write(path, contents).await
}
}
impl Local {
async fn copy_impl(from: PathBuf, to: PathBuf, cha: Cha) -> io::Result<u64> { async fn copy_impl(from: PathBuf, to: PathBuf, cha: Cha) -> io::Result<u64> {
let mut ft = std::fs::FileTimes::new(); let mut ft = std::fs::FileTimes::new();
cha.atime.map(|t| ft = ft.set_accessed(t)); cha.atime.map(|t| ft = ft.set_accessed(t));
@ -80,55 +261,6 @@ impl Local {
} }
} }
#[inline]
pub async fn create<P>(path: P) -> io::Result<RwFile>
where
P: AsRef<Path>,
{
Gate::default().write(true).create(true).truncate(true).open(path).await.map(Into::into)
}
#[inline]
pub async fn create_dir<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
tokio::fs::create_dir(path).await
}
#[inline]
pub async fn create_dir_all<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
tokio::fs::create_dir_all(path).await
}
#[inline]
pub async fn hard_link<P, Q>(original: P, link: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
tokio::fs::hard_link(original, link).await
}
#[inline]
pub async fn metadata<P>(path: P) -> io::Result<std::fs::Metadata>
where
P: AsRef<Path>,
{
tokio::fs::metadata(path).await
}
#[inline]
pub async fn open<P>(path: P) -> io::Result<RwFile>
where
P: AsRef<Path>,
{
Gate::default().read(true).open(path).await.map(Into::into)
}
#[inline] #[inline]
pub async fn read<P>(path: P) -> io::Result<Vec<u8>> pub async fn read<P>(path: P) -> io::Result<Vec<u8>>
where where
@ -137,22 +269,6 @@ impl Local {
tokio::fs::read(path).await tokio::fs::read(path).await
} }
#[inline]
pub async fn read_dir<P>(path: P) -> io::Result<ReadDir>
where
P: AsRef<Path>,
{
tokio::fs::read_dir(path).await.map(Into::into)
}
#[inline]
pub async fn read_link<P>(path: P) -> io::Result<PathBuf>
where
P: AsRef<Path>,
{
tokio::fs::read_link(path).await
}
#[inline] #[inline]
pub async fn read_to_string<P>(path: P) -> io::Result<String> pub async fn read_to_string<P>(path: P) -> io::Result<String>
where where
@ -160,130 +276,4 @@ impl Local {
{ {
tokio::fs::read_to_string(path).await tokio::fs::read_to_string(path).await
} }
#[inline]
pub async fn remove_dir<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
tokio::fs::remove_dir(path).await
}
#[inline]
pub async fn remove_dir_all<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
tokio::fs::remove_dir_all(path).await
}
#[inline]
pub async fn remove_file<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
tokio::fs::remove_file(path).await
}
#[inline]
pub async fn rename<P, Q>(from: P, to: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
tokio::fs::rename(from, to).await
}
#[inline]
pub async fn symlink<P, Q, F>(original: P, link: Q, _is_dir: F) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
F: AsyncFnOnce() -> io::Result<bool>,
{
#[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<P, Q>(original: P, link: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
#[cfg(unix)]
{
tokio::fs::symlink(original, link).await
}
#[cfg(windows)]
{
tokio::fs::symlink_dir(original, link).await
}
}
#[inline]
pub async fn symlink_file<P, Q>(original: P, link: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
#[cfg(unix)]
{
tokio::fs::symlink(original, link).await
}
#[cfg(windows)]
{
tokio::fs::symlink_file(original, link).await
}
}
#[inline]
pub async fn symlink_metadata<P>(path: P) -> io::Result<std::fs::Metadata>
where
P: AsRef<Path>,
{
tokio::fs::symlink_metadata(path).await
}
pub async fn trash<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
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<P, C>(path: P, contents: C) -> io::Result<()>
where
P: AsRef<Path>,
C: AsRef<[u8]>,
{
tokio::fs::write(path, contents).await
}
} }

View file

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

View file

@ -1,19 +1,13 @@
use std::io; 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<tokio::fs::ReadDir> for ReadDir { impl DirReader for ReadDir {
fn from(value: tokio::fs::ReadDir) -> Self { Self(value) } type Entry<'a> = super::DirEntry;
}
impl From<ReadDir> for crate::provider::ReadDir { async fn next(&mut self) -> io::Result<Option<Self::Entry<'_>>> {
fn from(value: ReadDir) -> Self { Self::Local(value) } self.0.next_entry().await.map(|entry| entry.map(super::DirEntry))
}
impl ReadDir {
pub async fn next_entry(&mut self) -> io::Result<Option<DirEntry>> {
self.0.next_entry().await.map(|entry| entry.map(Into::into))
} }
} }

View file

@ -1,78 +0,0 @@
use std::{pin::Pin, task::Poll};
use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite};
pub struct RwFile(tokio::fs::File);
impl From<tokio::fs::File> for RwFile {
fn from(value: tokio::fs::File) -> Self { Self(value) }
}
impl From<RwFile> 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<std::io::Result<()>> {
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<std::io::Result<u64>> {
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<Result<usize, std::io::Error>> {
Pin::new(&mut self.0).poll_write(cx, buf)
}
#[inline]
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
Pin::new(&mut self.0).poll_flush(cx)
}
#[inline]
fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
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<Result<usize, std::io::Error>> {
Pin::new(&mut self.0).poll_write_vectored(cx, bufs)
}
#[inline]
fn is_write_vectored(&self) -> bool { self.0.is_write_vectored() }
}

View file

@ -1,3 +1,5 @@
yazi_macro::mod_pub!(local sftp); 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(); }

View file

@ -2,7 +2,7 @@ use std::{io, path::{Path, PathBuf}};
use yazi_shared::url::{Url, UrlBuf}; 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] #[inline]
pub fn cache<'a, U>(url: U) -> Option<PathBuf> pub fn cache<'a, U>(url: U) -> Option<PathBuf>
@ -145,18 +145,6 @@ where
identical(a, b).await.unwrap_or(false) identical(a, b).await.unwrap_or(false)
} }
#[inline]
pub async fn open<'a, U>(url: U) -> io::Result<RwFile>
where
U: Into<Url<'a>>,
{
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] #[inline]
pub async fn read_dir<'a, U>(url: U) -> io::Result<ReadDir> pub async fn read_dir<'a, U>(url: U) -> io::Result<ReadDir>
where where

View file

@ -1,15 +1,20 @@
use std::io; use std::io;
use super::DirEntry; use super::DirEntry;
use crate::provider::DirReader;
pub enum ReadDir { pub enum ReadDir {
Local(super::local::ReadDir), Local(super::local::ReadDir),
} }
impl From<super::local::ReadDir> for ReadDir {
fn from(value: super::local::ReadDir) -> Self { Self::Local(value) }
}
impl ReadDir { impl ReadDir {
pub async fn next_entry(&mut self) -> io::Result<Option<DirEntry>> { pub async fn next_entry(&mut self) -> io::Result<Option<DirEntry>> {
match self { 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)),
} }
} }
} }

View file

@ -3,7 +3,16 @@ use std::pin::Pin;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
pub enum RwFile { pub enum RwFile {
Local(super::local::RwFile), Tokio(tokio::fs::File),
SFTP(Box<yazi_sftp::fs::File>),
}
impl From<tokio::fs::File> for RwFile {
fn from(f: tokio::fs::File) -> Self { Self::Tokio(f) }
}
impl From<yazi_sftp::fs::File> for RwFile {
fn from(f: yazi_sftp::fs::File) -> Self { Self::SFTP(Box::new(f)) }
} }
impl AsyncRead for RwFile { impl AsyncRead for RwFile {
@ -14,7 +23,8 @@ impl AsyncRead for RwFile {
buf: &mut tokio::io::ReadBuf<'_>, buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> { ) -> std::task::Poll<std::io::Result<()>> {
match &mut *self { 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], buf: &[u8],
) -> std::task::Poll<Result<usize, std::io::Error>> { ) -> std::task::Poll<Result<usize, std::io::Error>> {
match &mut *self { 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<'_>, cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), std::io::Error>> { ) -> std::task::Poll<Result<(), std::io::Error>> {
match &mut *self { 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<'_>, cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), std::io::Error>> { ) -> std::task::Poll<Result<(), std::io::Error>> {
match &mut *self { 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<'_>], bufs: &[std::io::IoSlice<'_>],
) -> std::task::Poll<Result<usize, std::io::Error>> { ) -> std::task::Poll<Result<usize, std::io::Error>> {
match &mut *self { 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] #[inline]
fn is_write_vectored(&self) -> bool { fn is_write_vectored(&self) -> bool {
match self { match self {
Self::Local(f) => f.is_write_vectored(), Self::Tokio(f) => f.is_write_vectored(),
Self::SFTP(f) => f.is_write_vectored(),
} }
} }
} }

View file

@ -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<P>(&self, path: P) -> io::Result<Self::File>
where
P: AsRef<Path>,
{
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
}
}

View file

@ -1 +1,6 @@
yazi_macro::mod_flat!(sftp); yazi_macro::mod_flat!(gate read_dir sftp);
pub(super) static CONN: yazi_shared::RoCell<deadpool::managed::Pool<Sftp>> =
yazi_shared::RoCell::new();
pub(super) fn init() { CONN.init(deadpool::managed::Pool::builder(Sftp).build().unwrap()); }

View file

@ -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<Option<Self::Entry<'_>>> {
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<std::fs::Metadata> { todo!() }
async fn file_type(&self) -> io::Result<std::fs::FileType> { todo!() }
}

View file

@ -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; pub struct Sftp;
impl Sftp { impl Provider for Sftp {
pub fn cache<P>(_: P) -> Option<PathBuf> type File = yazi_sftp::fs::File;
type Gate = super::Gate;
type ReadDir = super::ReadDir;
fn cache<P>(_: P) -> Option<PathBuf>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
todo!() todo!()
} }
pub async fn canonicalize<P>(path: P) -> io::Result<PathBuf> async fn canonicalize<P>(path: P) -> io::Result<PathBuf>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
todo!() Ok(Self::op().await?.realpath(&path).await?)
} }
pub async fn copy<P, Q>(from: P, to: Q, cha: Cha) -> io::Result<u64> async fn copy<P, Q>(from: P, to: Q, cha: Cha) -> io::Result<u64>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
{ {
todo!() // FIXME: pull this out to a From<Cha> 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<P>(path: P) -> io::Result<()> async fn create_dir<P>(path: P) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
todo!() Ok(Self::op().await?.mkdir(&path, Attrs::default()).await?)
} }
pub async fn create_dir<P>(path: P) -> io::Result<()> async fn hard_link<P, Q>(original: P, link: Q) -> io::Result<()>
where
P: AsRef<Path>,
{
todo!()
}
pub async fn create_dir_all<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
todo!()
}
pub async fn hard_link<P, Q>(original: P, link: Q) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
{ {
todo!() Ok(Self::op().await?.hardlink(&original, &link).await?)
} }
pub async fn metadata<P>(path: P) -> io::Result<std::fs::Metadata> async fn metadata<P>(path: P) -> io::Result<std::fs::Metadata>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
todo!() todo!()
} }
pub async fn open<P>(path: P) -> io::Result<()> async fn read_dir<P>(path: P) -> io::Result<Self::ReadDir>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
todo!() Ok(super::ReadDir(Self::op().await?.read_dir(&path).await?))
} }
pub async fn read<P>(path: P) -> io::Result<Vec<u8>> async fn read_link<P>(path: P) -> io::Result<PathBuf>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
todo!() Ok(Self::op().await?.readlink(&path).await?)
} }
pub async fn read_dir<P>(path: P) -> io::Result<()> async fn remove_dir<P>(path: P) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
todo!() Ok(Self::op().await?.rmdir(&path).await?)
} }
pub async fn read_link<P>(path: P) -> io::Result<PathBuf> async fn remove_file<P>(path: P) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
todo!() Ok(Self::op().await?.remove(&path).await?)
} }
pub async fn read_to_string<P>(path: P) -> io::Result<String> async fn rename<P, Q>(from: P, to: Q) -> io::Result<()>
where
P: AsRef<Path>,
{
todo!()
}
pub async fn remove_dir<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
todo!()
}
pub async fn remove_dir_all<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
todo!()
}
pub async fn remove_file<P>(path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
todo!()
}
pub async fn rename<P, Q>(from: P, to: Q) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
{ {
todo!() Ok(Self::op().await?.rename(&from, &to).await?)
} }
pub async fn symlink<P, Q, F>(original: P, link: Q, _is_dir: F) -> io::Result<()> async fn symlink<P, Q, F>(original: P, link: Q, _is_dir: F) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
F: AsyncFnOnce() -> io::Result<bool>, F: AsyncFnOnce() -> io::Result<bool>,
{ {
todo!() Ok(Self::op().await?.symlink(&original, &link).await?)
} }
pub async fn symlink_dir<P, Q>(original: P, link: Q) -> io::Result<()> async fn symlink_metadata<P>(path: P) -> io::Result<std::fs::Metadata>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
todo!()
}
pub async fn symlink_file<P, Q>(original: P, link: Q) -> io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
todo!()
}
pub async fn symlink_metadata<P>(path: P) -> io::Result<std::fs::Metadata>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
todo!() todo!()
} }
pub async fn trash<P>(path: P) -> io::Result<()> async fn trash<P>(_path: P) -> io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
todo!() Err(io::Error::new(io::ErrorKind::Unsupported, "Trash not supported"))
}
pub async fn write<P, C>(path: P, contents: C) -> io::Result<()>
where
P: AsRef<Path>,
C: AsRef<[u8]>,
{
todo!()
} }
} }
impl Sftp {
pub(super) async fn op() -> io::Result<deadpool::managed::Object<Sftp>> {
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<Self::Type, Self::Error> { todo!() }
async fn recycle(
&self,
obj: &mut Self::Type,
metrics: &deadpool::managed::Metrics,
) -> deadpool::managed::RecycleResult<Self::Error> {
todo!()
}
fn detach(&self, _obj: &mut Self::Type) { todo!() }
}

View file

@ -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<File = Self::File>;
type ReadDir: DirReader;
fn cache<P>(_: P) -> Option<PathBuf>
where
P: AsRef<Path>;
fn canonicalize<P>(path: P) -> impl Future<Output = io::Result<PathBuf>>
where
P: AsRef<Path>;
fn copy<P, Q>(from: P, to: Q, cha: Cha) -> impl Future<Output = io::Result<u64>>
where
P: AsRef<Path>,
Q: AsRef<Path>;
fn create<P>(path: P) -> impl Future<Output = io::Result<Self::File>>
where
P: AsRef<Path>,
{
async move { Self::Gate::default().write(true).create(true).truncate(true).open(path).await }
}
fn create_dir<P>(path: P) -> impl Future<Output = io::Result<()>>
where
P: AsRef<Path>;
fn create_dir_all<P>(path: P) -> impl Future<Output = io::Result<()>>
where
P: AsRef<Path>,
{
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<P, Q>(original: P, link: Q) -> impl Future<Output = io::Result<()>>
where
P: AsRef<Path>,
Q: AsRef<Path>;
fn metadata<P>(path: P) -> impl Future<Output = io::Result<std::fs::Metadata>>
where
P: AsRef<Path>;
fn open<P>(path: P) -> impl Future<Output = io::Result<Self::File>>
where
P: AsRef<Path>,
{
async move { Self::Gate::default().read(true).open(path).await }
}
fn read_dir<P>(path: P) -> impl Future<Output = io::Result<Self::ReadDir>>
where
P: AsRef<Path>;
fn read_link<P>(path: P) -> impl Future<Output = io::Result<PathBuf>>
where
P: AsRef<Path>;
fn remove_dir<P>(path: P) -> impl Future<Output = io::Result<()>>
where
P: AsRef<Path>;
fn remove_dir_all<P>(path: P) -> impl Future<Output = io::Result<()>>
where
P: AsRef<Path>,
{
async fn remove_dir_all_impl<S>(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::<S>(&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::<Self>(path).await
}
}
}
fn remove_file<P>(path: P) -> impl Future<Output = io::Result<()>>
where
P: AsRef<Path>;
fn rename<P, Q>(from: P, to: Q) -> impl Future<Output = io::Result<()>>
where
P: AsRef<Path>,
Q: AsRef<Path>;
fn symlink<P, Q, F>(original: P, link: Q, _is_dir: F) -> impl Future<Output = io::Result<()>>
where
P: AsRef<Path>,
Q: AsRef<Path>,
F: AsyncFnOnce() -> io::Result<bool>;
fn symlink_dir<P, Q>(original: P, link: Q) -> impl Future<Output = io::Result<()>>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
Self::symlink(original, link, async || Ok(true))
}
fn symlink_file<P, Q>(original: P, link: Q) -> impl Future<Output = io::Result<()>>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
Self::symlink(original, link, async || Ok(false))
}
fn symlink_metadata<P>(path: P) -> impl Future<Output = io::Result<std::fs::Metadata>>
where
P: AsRef<Path>;
fn trash<P>(path: P) -> impl Future<Output = io::Result<()>>
where
P: AsRef<Path>;
fn write<P, C>(path: P, contents: C) -> impl Future<Output = io::Result<()>>
where
P: AsRef<Path>,
C: AsRef<[u8]>,
{
async move { Self::create(path).await?.write_all(contents.as_ref()).await }
}
}
// --- DirReader
pub trait DirReader {
type Entry<'a>: FileHolder
where
Self: 'a;
fn next(&mut self) -> impl Future<Output = io::Result<Option<Self::Entry<'_>>>>;
}
// --- FileHolder
pub trait FileHolder {
fn path(&self) -> PathBuf;
fn name(&self) -> Cow<'_, OsStr>;
fn metadata(&self) -> impl Future<Output = io::Result<std::fs::Metadata>>;
fn file_type(&self) -> impl Future<Output = io::Result<std::fs::FileType>>;
}
// --- 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<P>(&self, path: P) -> impl Future<Output = io::Result<Self::File>>
where
P: AsRef<Path>;
fn read(&mut self, read: bool) -> &mut Self;
fn truncate(&mut self, truncate: bool) -> &mut Self;
fn write(&mut self, write: bool) -> &mut Self;
}

13
yazi-macro/src/fs.rs Normal file
View file

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

View file

@ -3,6 +3,7 @@ mod asset;
mod context; mod context;
mod event; mod event;
mod fmt; mod fmt;
mod fs;
mod log; mod log;
mod module; mod module;
mod platform; mod platform;

View file

@ -57,9 +57,9 @@ end
function Linemode:permissions() return self._file.cha:perm() or "" end function Linemode:permissions() return self._file.cha:perm() or "" end
function Linemode:owner() function Linemode:owner()
local user = self._file.cha.uid and ya.user_name(self._file.cha.uid) or self._file.cha.uid local user = ya.user_name 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 local group = ya.group_name and ya.group_name(self._file.cha.gid) or self._file.cha.gid
return string.format("%s:%s", user or "-", group or "-") return string.format("%s:%s", user, group)
end end
function Linemode:redraw() function Linemode:redraw()

View file

@ -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 syntect::{LoadingError, dumps, easy::HighlightLines, highlighting::{self, Theme, ThemeSet}, parsing::{SyntaxReference, SyntaxSet}};
use tokio::io::{AsyncBufReadExt, AsyncSeekExt, BufReader}; use tokio::io::{AsyncBufReadExt, AsyncSeekExt, BufReader};
use yazi_config::{THEME, YAZI, preview::PreviewWrap}; 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}; use yazi_shared::{Ids, errors::PeekError, replace_to_printable};
static INCR: Ids = Ids::new(); static INCR: Ids = Ids::new();
@ -132,7 +132,7 @@ impl Highlighter {
async fn find_syntax( async fn find_syntax(
path: &Path, path: &Path,
reader: &mut BufReader<local::RwFile>, reader: &mut BufReader<tokio::fs::File>,
) -> Result<&'static SyntaxReference> { ) -> Result<&'static SyntaxReference> {
let (_, syntaxes) = Self::init(); let (_, syntaxes) = Self::init();
let name = path.file_name().map(|n| n.to_string_lossy()).unwrap_or_default(); let name = path.file_name().map(|n| n.to_string_lossy()).unwrap_or_default();

View file

@ -38,6 +38,13 @@ impl<'a> From<&'a Path> for ByteStr<'a> {
fn from(value: &'a Path) -> Self { ByteStr::from(value.as_os_str()) } fn from(value: &'a Path) -> Self { ByteStr::from(value.as_os_str()) }
} }
impl<'a, T> From<&'a T> for ByteStr<'a>
where
T: AsRef<Path>,
{
fn from(value: &'a T) -> Self { Self::from(value.as_ref()) }
}
impl PartialEq<&str> for ByteStr<'_> { impl PartialEq<&str> for ByteStr<'_> {
fn eq(&self, other: &&str) -> bool { self.0 == other.as_bytes() } 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 { pub(super) unsafe fn from_str_bytes_unchecked(bytes: &'a [u8]) -> Self {
Self(Cow::Borrowed(bytes)) Self(Cow::Borrowed(bytes))
} }

View file

@ -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 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> { pub struct File {
session: &'a Session, session: Arc<Session>,
handle: String, handle: String,
closed: bool, closed: bool,
@ -16,39 +16,41 @@ pub struct File<'a> {
flush_rx: Option<Timeout<oneshot::Receiver<Packet<'static>>>>, flush_rx: Option<Timeout<oneshot::Receiver<Packet<'static>>>>,
} }
impl Unpin for File<'_> {} impl Unpin for File {}
impl Drop for File<'_> { impl Drop for File {
fn drop(&mut self) { fn drop(&mut self) {
if !self.closed { if !self.closed {
self.session.close(&self.handle).ok(); Operator::from(&self.session).close(&self.handle).ok();
} }
} }
} }
impl<'a> File<'a> { impl File {
pub(crate) fn new(session: &'a Session, handle: impl Into<String>) -> Self { pub(crate) fn new(session: &Arc<Session>, handle: impl Into<String>) -> Self {
Self { Self {
session, session: session.clone(),
handle: handle.into(), handle: handle.into(),
closed: false, closed: false,
cursor: 0, cursor: 0,
close_rx: None, close_rx: None,
read_rx: None, read_rx: None,
write_rx: None, write_rx: None,
flush_rx: None, flush_rx: None,
} }
} }
pub async fn fstat(&self) -> Result<Attrs, Error> { self.session.fstat(&self.handle).await } pub async fn fstat(&self) -> Result<Attrs, Error> {
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> {
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( fn poll_read(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
@ -58,7 +60,7 @@ impl AsyncRead for File<'_> {
if me.read_rx.is_none() { if me.read_rx.is_none() {
let max = buf.remaining().min(261120) as u32; 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)); 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( fn poll_write(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
@ -91,7 +93,7 @@ impl AsyncWrite for File<'_> {
Some((rx, len)) => (rx, *len), Some((rx, len)) => (rx, *len),
None => { None => {
let max = buf.len().min(261120); 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) (&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() }; let me = unsafe { self.get_unchecked_mut() };
if me.flush_rx.is_none() { 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)), Ok(rx) => me.flush_rx = Some(timeout(Duration::from_secs(10), rx)),
Err(Error::Unsupported) => return Poll::Ready(Ok(())), Err(Error::Unsupported) => return Poll::Ready(Ok(())),
Err(e) => Err(e)?, Err(e) => Err(e)?,
@ -141,7 +143,8 @@ impl AsyncWrite for File<'_> {
let me = unsafe { self.get_unchecked_mut() }; let me = unsafe { self.get_unchecked_mut() };
if me.close_rx.is_none() { 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()) }; let rx = unsafe { Pin::new_unchecked(me.close_rx.as_mut().unwrap()) };

View file

@ -1,20 +1,28 @@
use std::mem; use std::{mem, sync::Arc};
use crate::{ByteStr, Error, Session, fs::DirEntry, requests, responses}; use crate::{ByteStr, Error, Session, fs::DirEntry, requests, responses};
pub struct ReadDir<'a> { pub struct ReadDir {
dir: ByteStr<'a>, session: Arc<Session>,
dir: ByteStr<'static>,
handle: String, handle: String,
session: &'a Session,
name: responses::Name<'a>, name: responses::Name<'static>,
cursor: usize, cursor: usize,
done: bool, done: bool,
} }
impl<'a> ReadDir<'a> { impl ReadDir {
pub(crate) fn new(session: &'a Session, dir: ByteStr<'a>, handle: String) -> Self { pub(crate) fn new(session: &Arc<Session>, dir: ByteStr, handle: String) -> Self {
Self { dir, handle, session, name: Default::default(), cursor: 0, done: false } Self {
session: session.clone(),
dir: dir.into_owned(),
handle,
name: Default::default(),
cursor: 0,
done: false,
}
} }
pub async fn next(&mut self) -> Result<Option<DirEntry<'_>>, Error> { pub async fn next(&mut self) -> Result<Option<DirEntry<'_>>, Error> {

View file

@ -9,6 +9,7 @@ mod de;
mod error; mod error;
mod id; mod id;
mod macros; mod macros;
mod operator;
mod packet; mod packet;
mod ser; mod ser;
mod session; mod session;
@ -17,6 +18,7 @@ pub use byte_str::*;
pub(crate) use de::*; pub(crate) use de::*;
pub use error::*; pub use error::*;
pub(crate) use id::*; pub(crate) use id::*;
pub use operator::*;
pub use packet::*; pub use packet::*;
pub(crate) use ser::*; pub(crate) use ser::*;
pub use session::*; pub use session::*;

197
yazi-sftp/src/operator.rs Normal file
View file

@ -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<Session>);
impl Deref for Operator {
type Target = Session;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl From<&Arc<Session>> for Operator {
fn from(session: &Arc<Session>) -> 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<File, Error>
where
P: Into<ByteStr<'a>>,
{
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<oneshot::Receiver<Packet<'static>>, Error> {
self.send_sync(requests::Close::new(handle))
}
pub fn read(
&self,
handle: &str,
offset: u64,
len: u32,
) -> Result<oneshot::Receiver<Packet<'static>>, Error> {
self.send_sync(requests::Read::new(handle, offset, len))
}
pub fn write(
&self,
handle: &str,
offset: u64,
data: &[u8],
) -> Result<oneshot::Receiver<Packet<'static>>, Error> {
self.send_sync(requests::Write::new(handle, offset, data))
}
pub async fn lstat<'a, P>(&self, path: P) -> Result<Attrs, Error>
where
P: Into<ByteStr<'a>>,
{
let attrs: responses::Attrs = self.send(requests::Lstat::new(path)).await?;
Ok(attrs.attrs)
}
pub async fn fstat(&self, handle: &str) -> Result<Attrs, Error> {
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<ByteStr<'a>>,
{
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<ByteStr<'a>>) -> Result<ReadDir, Error> {
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<ByteStr<'a>>,
{
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<ByteStr<'a>>,
{
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<ByteStr<'a>>,
{
let status: responses::Status = self.send(requests::Rmdir::new(path)).await?;
status.into()
}
pub async fn realpath<'a, P>(&self, path: P) -> Result<PathBuf, Error>
where
P: Into<ByteStr<'a>>,
{
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<Attrs, Error>
where
P: Into<ByteStr<'a>>,
{
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<ByteStr<'a>>,
T: Into<ByteStr<'a>>,
{
let status: responses::Status = self.send(requests::Rename::new(from, to)).await?;
status.into()
}
pub async fn readlink<'a, P>(&self, path: P) -> Result<PathBuf, Error>
where
P: Into<ByteStr<'a>>,
{
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<ByteStr<'a>>,
L: Into<ByteStr<'a>>,
{
let status: responses::Status = self.send(requests::Symlink::new(original, link)).await?;
status.into()
}
pub fn fsync(&self, handle: &str) -> Result<oneshot::Receiver<Packet<'static>>, 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<ByteStr<'a>>,
L: Into<ByteStr<'a>>,
{
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<responses::ExtendedLimits, Error> {
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()
}
}

View file

@ -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 parking_lot::Mutex;
use russh::{ChannelStream, client::Msg}; use russh::{ChannelStream, client::Msg};
use serde::Serialize; use serde::Serialize;
use tokio::{io::{AsyncReadExt, AsyncWriteExt}, select, sync::{mpsc, oneshot}}; use tokio::{io::{AsyncReadExt, AsyncWriteExt}, 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 { pub struct Session {
tx: mpsc::UnboundedSender<Vec<u8>>, tx: mpsc::UnboundedSender<Vec<u8>>,
pub(crate) id: Id, id: Id,
callback: Arc<Mutex<HashMap<u32, oneshot::Sender<Packet<'static>>>>>, callback: Mutex<HashMap<u32, oneshot::Sender<Packet<'static>>>>,
extensions: HashMap<String, String>, pub(super) extensions: Mutex<HashMap<String, String>>,
} }
impl Session { impl Session {
pub fn make(stream: ChannelStream<Msg>) -> Self { pub fn make(stream: ChannelStream<Msg>) -> Arc<Self> {
let (tx, mut rx) = mpsc::unbounded_channel::<Vec<u8>>(); let (tx, mut rx) = mpsc::unbounded_channel();
let (mut reader, mut writer) = tokio::io::split(stream); let me = Arc::new(Self {
let callback = Arc::new(Mutex::new(HashMap::<_, oneshot::Sender<_>>::new())); tx,
id: Id::default(),
callback: Default::default(),
extensions: Default::default(),
});
let (mut reader, mut writer) = tokio::io::split(stream);
tokio::spawn(async move { tokio::spawn(async move {
while let Some(data) = rx.recv().await { while let Some(data) = rx.recv().await {
if data.is_empty() { if data.is_empty() {
@ -32,206 +37,28 @@ impl Session {
} }
}); });
let (tx_, callback_) = (tx.clone(), callback.clone()); let me_ = me.clone();
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {
select! { select! {
Ok(len) = reader.read_u32() => { Ok(len) = reader.read_u32() => {
let mut buf = vec![0; len as usize]; let mut buf = vec![0; len as usize];
if let Err(e) = reader.read_exact(&mut buf).await && e.kind() == ErrorKind::UnexpectedEof { if let Err(e) = reader.read_exact(&mut buf).await && e.kind() == ErrorKind::UnexpectedEof {
tx_.send(vec![]).ok(); me_.tx.send(vec![]).ok();
break; break;
} }
if let Ok(packet) = crate::from_bytes(&buf) 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(); cb.send(packet).ok();
} }
} }
_ = tx_.closed() => break, _ = me_.tx.closed() => break,
} }
} }
}); });
Self { tx, id: Id::default(), callback, extensions: HashMap::new() } me
}
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<File<'_>, Error>
where
P: Into<ByteStr<'a>>,
{
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<oneshot::Receiver<Packet<'static>>, Error> {
self.send_sync(requests::Close::new(handle))
}
pub fn read(
&self,
handle: &str,
offset: u64,
len: u32,
) -> Result<oneshot::Receiver<Packet<'static>>, Error> {
self.send_sync(requests::Read::new(handle, offset, len))
}
pub fn write(
&self,
handle: &str,
offset: u64,
data: &[u8],
) -> Result<oneshot::Receiver<Packet<'static>>, Error> {
self.send_sync(requests::Write::new(handle, offset, data))
}
pub async fn lstat<'a, P>(&self, path: P) -> Result<Attrs, Error>
where
P: Into<ByteStr<'a>>,
{
let attrs: responses::Attrs = self.send(requests::Lstat::new(path)).await?;
Ok(attrs.attrs)
}
pub async fn fstat(&self, handle: &str) -> Result<Attrs, Error> {
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<ByteStr<'a>>,
{
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<ByteStr<'a>>) -> Result<ReadDir<'a>, 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<ByteStr<'a>>,
{
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<ByteStr<'a>>,
{
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<ByteStr<'a>>,
{
let status: responses::Status = self.send(requests::Rmdir::new(path)).await?;
status.into()
}
pub async fn realpath<'a, P>(&self, path: P) -> Result<PathBuf, Error>
where
P: Into<ByteStr<'a>>,
{
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<Attrs, Error>
where
P: Into<ByteStr<'a>>,
{
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<ByteStr<'a>>,
T: Into<ByteStr<'a>>,
{
let status: responses::Status = self.send(requests::Rename::new(from, to)).await?;
status.into()
}
pub async fn readlink<'a, P>(&self, path: P) -> Result<PathBuf, Error>
where
P: Into<ByteStr<'a>>,
{
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<ByteStr<'a>>,
O: Into<ByteStr<'a>>,
{
let status: responses::Status = self.send(requests::Symlink::new(link, original)).await?;
status.into()
}
pub fn fsync(&self, handle: &str) -> Result<oneshot::Receiver<Packet<'static>>, 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<ByteStr<'a>>,
L: Into<ByteStr<'a>>,
{
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<responses::ExtendedLimits, Error> {
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()
} }
pub async fn send<'a, I, O>(&self, input: I) -> Result<O, Error> pub async fn send<'a, I, O>(&self, input: I) -> Result<O, Error>

View file

@ -53,9 +53,7 @@ impl<'a> TryFrom<&'a [u8]> for UrlCow<'a> {
Self::Borrowed { loc: Loc::with(p, uri, urn)?, scheme } Self::Borrowed { loc: Loc::with(p, uri, urn)?, scheme }
} }
(Cow::Owned(p), None) => Self::Owned { loc: LocBuf::from(p), scheme }, (Cow::Owned(p), None) => Self::Owned { loc: LocBuf::from(p), scheme },
(Cow::Owned(p), Some((uri, urn))) => { (Cow::Owned(p), Some((uri, urn))) => Self::Owned { loc: LocBuf::with(p, uri, urn)?, scheme },
Self::Owned { loc: LocBuf::with(p, uri, urn)?, scheme }
}
}) })
} }
} }

View file

@ -13,7 +13,7 @@ pub(crate) struct Backend {
impl Backend { impl Backend {
pub(crate) fn serve(out_tx: mpsc::UnboundedSender<UrlBuf>) -> Self { pub(crate) fn serve(out_tx: mpsc::UnboundedSender<UrlBuf>) -> Self {
#[cfg(any(target_os = "linux", target_os = "macos"))] #[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()) yazi_macro::err!(yazi_dds::Pubsub::pub_after_mount())
}); });