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"
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",
]

View file

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

View file

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

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 ratatui::layout::Rect;
use yazi_config::YAZI;
use yazi_fs::provider::local::Local;
use yazi_fs::provider::{Provider, local::Local};
use crate::Dimension;

View file

@ -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<M: UserDataMethods<Self>>(methods: &mut M) {

View file

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

View file

@ -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<String> =
["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()

View file

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

View file

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

View file

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

View file

@ -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<Path>) -> bool {

View file

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

View file

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

View file

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

View file

@ -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 }

View file

@ -13,16 +13,11 @@ pub struct Cha {
pub len: u64,
pub atime: Option<SystemTime>,
pub btime: Option<SystemTime>,
#[cfg(unix)]
pub ctime: Option<SystemTime>,
pub mtime: Option<SystemTime>,
#[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 _,
}
}

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

View file

@ -9,22 +9,22 @@ use yazi_shared::{natsort, replace_cow, replace_vec_cow};
use super::{Locked, Partition, Partitions};
impl Partitions {
pub fn monitor<F>(me: Locked, cb: F)
pub fn monitor<F>(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) {

View file

@ -12,7 +12,7 @@ use yazi_shared::natsort;
use super::{Locked, Partition, Partitions};
impl Partitions {
pub fn monitor<F>(me: Locked, cb: F)
pub fn monitor<F>(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<dyn Fn()> = 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 {

View file

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

View file

@ -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<RwLock<Partitions>>;
pub(super) type Locked = RwLock<Partitions>;
pub static PARTITIONS: RoCell<Locked> = 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))
}

View file

@ -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<super::local::DirEntry> 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(),
}
}

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 {
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<tokio::fs::DirEntry> for DirEntry {
fn from(value: tokio::fs::DirEntry) -> Self { Self(value) }
}
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() }
fn name(&self) -> Cow<'_, OsStr> { self.0.file_name().into() }
async fn metadata(&self) -> io::Result<std::fs::Metadata> { self.0.metadata().await }
async fn file_type(&self) -> io::Result<std::fs::FileType> { self.0.file_type().await }
}

View file

@ -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<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 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>(_: P) -> Option<PathBuf>
fn cache<P>(_: P) -> Option<PathBuf>
where
P: AsRef<Path>,
{
@ -14,7 +18,7 @@ impl Local {
}
#[inline]
pub async fn canonicalize<P>(path: P) -> io::Result<PathBuf>
async fn canonicalize<P>(path: P) -> io::Result<PathBuf>
where
P: AsRef<Path>,
{
@ -22,7 +26,7 @@ impl Local {
}
#[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
P: AsRef<Path>,
Q: AsRef<Path>,
@ -32,6 +36,183 @@ impl Local {
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> {
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<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]
pub async fn read<P>(path: P) -> io::Result<Vec<u8>>
where
@ -137,22 +269,6 @@ impl Local {
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]
pub async fn read_to_string<P>(path: P) -> io::Result<String>
where
@ -160,130 +276,4 @@ impl Local {
{
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 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 {
fn from(value: tokio::fs::ReadDir) -> Self { Self(value) }
}
impl DirReader for ReadDir {
type Entry<'a> = super::DirEntry;
impl From<ReadDir> for crate::provider::ReadDir {
fn from(value: ReadDir) -> Self { Self::Local(value) }
}
impl ReadDir {
pub async fn next_entry(&mut self) -> io::Result<Option<DirEntry>> {
self.0.next_entry().await.map(|entry| entry.map(Into::into))
async fn next(&mut self) -> io::Result<Option<Self::Entry<'_>>> {
self.0.next_entry().await.map(|entry| entry.map(super::DirEntry))
}
}

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_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 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<PathBuf>
@ -145,18 +145,6 @@ where
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]
pub async fn read_dir<'a, U>(url: U) -> io::Result<ReadDir>
where

View file

@ -1,15 +1,20 @@
use std::io;
use super::DirEntry;
use crate::provider::DirReader;
pub enum ReadDir {
Local(super::local::ReadDir),
}
impl From<super::local::ReadDir> for ReadDir {
fn from(value: super::local::ReadDir) -> Self { Self::Local(value) }
}
impl ReadDir {
pub async fn next_entry(&mut self) -> io::Result<Option<DirEntry>> {
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};
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 {
@ -14,7 +23,8 @@ impl AsyncRead for RwFile {
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
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<Result<usize, std::io::Error>> {
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<Result<(), std::io::Error>> {
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<Result<(), std::io::Error>> {
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<Result<usize, std::io::Error>> {
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(),
}
}
}

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;
impl Sftp {
pub fn cache<P>(_: P) -> Option<PathBuf>
impl Provider for Sftp {
type File = yazi_sftp::fs::File;
type Gate = super::Gate;
type ReadDir = super::ReadDir;
fn cache<P>(_: P) -> Option<PathBuf>
where
P: AsRef<Path>,
{
todo!()
}
pub async fn canonicalize<P>(path: P) -> io::Result<PathBuf>
async fn canonicalize<P>(path: P) -> io::Result<PathBuf>
where
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
P: 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
P: AsRef<Path>,
{
todo!()
Ok(Self::op().await?.mkdir(&path, Attrs::default()).await?)
}
pub async fn create_dir<P>(path: P) -> 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<()>
async fn hard_link<P, Q>(original: P, link: Q) -> io::Result<()>
where
P: 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
P: AsRef<Path>,
{
todo!()
}
pub async fn open<P>(path: P) -> io::Result<()>
async fn read_dir<P>(path: P) -> io::Result<Self::ReadDir>
where
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
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
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
P: AsRef<Path>,
{
todo!()
Ok(Self::op().await?.remove(&path).await?)
}
pub async fn read_to_string<P>(path: P) -> io::Result<String>
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<()>
async fn rename<P, Q>(from: P, to: Q) -> io::Result<()>
where
P: 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
P: AsRef<Path>,
Q: AsRef<Path>,
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<()>
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>
async fn symlink_metadata<P>(path: P) -> io::Result<std::fs::Metadata>
where
P: AsRef<Path>,
{
todo!()
}
pub async fn trash<P>(path: P) -> io::Result<()>
async fn trash<P>(_path: P) -> io::Result<()>
where
P: AsRef<Path>,
{
todo!()
}
pub async fn write<P, C>(path: P, contents: C) -> io::Result<()>
where
P: AsRef<Path>,
C: AsRef<[u8]>,
{
todo!()
Err(io::Error::new(io::ErrorKind::Unsupported, "Trash not supported"))
}
}
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 event;
mod fmt;
mod fs;
mod log;
mod module;
mod platform;

View file

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

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 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<local::RwFile>,
reader: &mut BufReader<tokio::fs::File>,
) -> Result<&'static SyntaxReference> {
let (_, syntaxes) = Self::init();
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()) }
}
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<'_> {
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))
}

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 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<Session>,
handle: String,
closed: bool,
@ -16,39 +16,41 @@ pub struct File<'a> {
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) {
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<String>) -> Self {
impl File {
pub(crate) fn new(session: &Arc<Session>, handle: impl Into<String>) -> 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<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> {
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()) };

View file

@ -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<Session>,
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<Session>, 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<Option<DirEntry<'_>>, Error> {

View file

@ -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::*;

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 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<Vec<u8>>,
pub(crate) id: Id,
callback: Arc<Mutex<HashMap<u32, oneshot::Sender<Packet<'static>>>>>,
extensions: HashMap<String, String>,
tx: mpsc::UnboundedSender<Vec<u8>>,
id: Id,
callback: Mutex<HashMap<u32, oneshot::Sender<Packet<'static>>>>,
pub(super) extensions: Mutex<HashMap<String, String>>,
}
impl Session {
pub fn make(stream: ChannelStream<Msg>) -> Self {
let (tx, mut rx) = mpsc::unbounded_channel::<Vec<u8>>();
let (mut reader, mut writer) = tokio::io::split(stream);
let callback = Arc::new(Mutex::new(HashMap::<_, oneshot::Sender<_>>::new()));
pub fn make(stream: ChannelStream<Msg>) -> Arc<Self> {
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<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()
me
}
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 }
}
(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 },
})
}
}

View file

@ -13,7 +13,7 @@ pub(crate) struct Backend {
impl Backend {
pub(crate) fn serve(out_tx: mpsc::UnboundedSender<UrlBuf>) -> 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())
});