mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 14:51:03 +00:00
feat: casefold support for the SFTP provider (#3201)
This commit is contained in:
parent
9f39363dd4
commit
6b09f2b665
13 changed files with 95 additions and 32 deletions
|
|
@ -1,17 +1,15 @@
|
|||
use std::{io, path::{Path, PathBuf}};
|
||||
|
||||
#[inline]
|
||||
pub async fn casefold(path: impl AsRef<Path>) -> io::Result<PathBuf> {
|
||||
let path = path.as_ref().to_owned();
|
||||
tokio::task::spawn_blocking(move || casefold_impl(path)).await?
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn must_case_match(path: impl AsRef<Path>) -> bool {
|
||||
let path = path.as_ref();
|
||||
casefold(path).await.is_ok_and(|p| p == path)
|
||||
}
|
||||
|
||||
pub(super) async fn casefold(path: impl AsRef<Path>) -> io::Result<PathBuf> {
|
||||
let path = path.as_ref().to_owned();
|
||||
tokio::task::spawn_blocking(move || casefold_impl(path)).await?
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "macos",
|
||||
target_os = "netbsd",
|
||||
|
|
@ -95,7 +93,7 @@ fn casefold_impl(path: PathBuf) -> io::Result<PathBuf> {
|
|||
// Case-insensitive match
|
||||
Ok(entries.swap_remove(i))
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::NotFound, "file not found"))
|
||||
Err(io::Error::from(io::ErrorKind::NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,13 @@ impl Provider for Local {
|
|||
tokio::fs::canonicalize(path).await
|
||||
}
|
||||
|
||||
async fn casefold<P>(&self, path: P) -> io::Result<PathBuf>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
super::casefold(path).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn copy<P, Q>(&self, from: P, to: Q, cha: Cha) -> io::Result<u64>
|
||||
where
|
||||
|
|
|
|||
|
|
@ -67,11 +67,16 @@ pub async fn casefold<'a, U>(url: U) -> io::Result<UrlBuf>
|
|||
where
|
||||
U: Into<Url<'a>>,
|
||||
{
|
||||
if let Some(path) = url.into().as_path() {
|
||||
local::casefold(path).await.map(Into::into)
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem"))
|
||||
}
|
||||
let url: Url = url.into();
|
||||
let fold = Providers::new(url).await?.casefold(url.loc).await?;
|
||||
|
||||
Ok(match url.scheme {
|
||||
SchemeRef::Regular | SchemeRef::Search(_) => fold.into(),
|
||||
SchemeRef::Archive(_) => {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem: archive"))?
|
||||
}
|
||||
SchemeRef::Sftp(_) => UrlBuf { loc: fold.into(), scheme: url.scheme.into() },
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn copy<'a, U, V>(from: U, to: V, cha: Cha) -> io::Result<u64>
|
||||
|
|
@ -134,7 +139,7 @@ where
|
|||
if original.scheme.covariant(link.scheme) {
|
||||
Providers::new(original).await?.hard_link(original.loc, link.loc).await
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::CrossesDevices, "Cross-filesystem hardlink"))
|
||||
Err(io::Error::from(io::ErrorKind::CrossesDevices))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -215,7 +220,7 @@ where
|
|||
if from.scheme.covariant(to.scheme) {
|
||||
Providers::new(from).await?.rename(from.loc, to.loc).await
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::CrossesDevices, "Cross-filesystem rename"))
|
||||
Err(io::Error::from(io::ErrorKind::CrossesDevices))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,16 @@ impl Provider for Providers<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
async fn casefold<P>(&self, path: P) -> io::Result<PathBuf>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
match self.0 {
|
||||
Inner::Regular | Inner::Search(_) => Local.casefold(path).await,
|
||||
Inner::Sftp((p, _)) => p.casefold(path).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn copy<P, Q>(&self, from: P, to: Q, cha: Cha) -> io::Result<u64>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ use std::{ffi::OsStr, io, time::{Duration, UNIX_EPOCH}};
|
|||
|
||||
use crate::cha::{Cha, ChaKind, ChaMode};
|
||||
|
||||
impl TryFrom<yazi_sftp::fs::DirEntry> for Cha {
|
||||
impl TryFrom<&yazi_sftp::fs::DirEntry> for Cha {
|
||||
type Error = io::Error;
|
||||
|
||||
fn try_from(ent: yazi_sftp::fs::DirEntry) -> Result<Self, Self::Error> {
|
||||
fn try_from(ent: &yazi_sftp::fs::DirEntry) -> Result<Self, Self::Error> {
|
||||
let mut cha = Self::try_from((ent.name().as_ref(), ent.attrs()))?;
|
||||
cha.nlink = ent.nlink().unwrap_or_default();
|
||||
Ok(cha)
|
||||
|
|
|
|||
|
|
@ -20,10 +20,7 @@ impl FileHolder for DirEntry {
|
|||
|
||||
fn name(&self) -> Cow<'_, OsStr> { self.0.name() }
|
||||
|
||||
async fn metadata(&self) -> io::Result<Cha> {
|
||||
let name = self.name();
|
||||
(name.as_ref(), self.0.attrs()).try_into()
|
||||
}
|
||||
async fn metadata(&self) -> io::Result<Cha> { Cha::try_from(&self.0) }
|
||||
|
||||
async fn file_type(&self) -> io::Result<ChaType> {
|
||||
ChaMode::try_from(self.0.attrs()).map(Into::into)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use yazi_sftp::fs::{Attrs, Flags};
|
|||
use yazi_shared::{scheme::SchemeRef, url::{Url, UrlBuf, UrlCow}};
|
||||
use yazi_vfs::config::ProviderSftp;
|
||||
|
||||
use crate::{cha::Cha, provider::{FileBuilder, Provider, local::Local}};
|
||||
use crate::{cha::Cha, provider::{DirReader, FileBuilder, FileHolder, Provider, local::Local}};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Sftp {
|
||||
|
|
@ -44,6 +44,38 @@ impl Provider for Sftp {
|
|||
Ok(self.op().await?.realpath(&path).await?)
|
||||
}
|
||||
|
||||
async fn casefold<P>(&self, path: P) -> io::Result<PathBuf>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let Some((parent, name)) = path.parent().zip(path.file_name()) else {
|
||||
return Ok(path.to_owned());
|
||||
};
|
||||
|
||||
if !self.symlink_metadata(path).await?.is_link() {
|
||||
return match self.canonicalize(path).await?.file_name() {
|
||||
Some(name) => Ok(parent.join(name)),
|
||||
None => Err(io::Error::other("Cannot get filename")),
|
||||
};
|
||||
}
|
||||
|
||||
let mut it = self.read_dir(parent).await?;
|
||||
let mut similar = None;
|
||||
while let Some(entry) = it.next().await? {
|
||||
let s = entry.name();
|
||||
if !s.eq_ignore_ascii_case(name) {
|
||||
continue;
|
||||
} else if s == name {
|
||||
return Ok(entry.path());
|
||||
} else if similar.is_none() {
|
||||
similar = Some(s.into_owned());
|
||||
}
|
||||
}
|
||||
|
||||
similar.map(|n| parent.join(n)).ok_or_else(|| io::Error::from(io::ErrorKind::NotFound))
|
||||
}
|
||||
|
||||
async fn copy<P, Q>(&self, from: P, to: Q, cha: Cha) -> io::Result<u64>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ pub trait Provider {
|
|||
where
|
||||
P: AsRef<Path>;
|
||||
|
||||
fn casefold<P>(&self, path: P) -> impl Future<Output = io::Result<PathBuf>>
|
||||
where
|
||||
P: AsRef<Path>;
|
||||
|
||||
fn copy<P, Q>(&self, from: P, to: Q, cha: Cha) -> impl Future<Output = io::Result<u64>>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
|
|
|
|||
|
|
@ -31,11 +31,21 @@ impl From<Error> for std::io::Error {
|
|||
fn from(err: Error) -> Self {
|
||||
match err {
|
||||
Error::IO(e) => e,
|
||||
Error::Serde(_) => Self::new(std::io::ErrorKind::InvalidData, err),
|
||||
Error::Status(_) => Self::other(err),
|
||||
Error::Packet(_) => Self::new(std::io::ErrorKind::InvalidData, err),
|
||||
Error::Timeout => Self::new(std::io::ErrorKind::TimedOut, err),
|
||||
Error::Unsupported => Self::new(std::io::ErrorKind::Unsupported, err),
|
||||
Error::Serde(e) => Self::new(std::io::ErrorKind::InvalidData, e),
|
||||
Error::Status(status) => match status.code {
|
||||
responses::StatusCode::Ok => Self::new(std::io::ErrorKind::Other, "unexpected OK"),
|
||||
responses::StatusCode::Eof => Self::from(std::io::ErrorKind::UnexpectedEof),
|
||||
responses::StatusCode::NoSuchFile => Self::from(std::io::ErrorKind::NotFound),
|
||||
responses::StatusCode::PermissionDenied => Self::from(std::io::ErrorKind::PermissionDenied),
|
||||
responses::StatusCode::Failure => Self::from(std::io::ErrorKind::Other),
|
||||
responses::StatusCode::BadMessage => Self::from(std::io::ErrorKind::InvalidData),
|
||||
responses::StatusCode::NoConnection => Self::from(std::io::ErrorKind::NotConnected),
|
||||
responses::StatusCode::ConnectionLost => Self::from(std::io::ErrorKind::ConnectionReset),
|
||||
responses::StatusCode::OpUnsupported => Self::from(std::io::ErrorKind::Unsupported),
|
||||
},
|
||||
Error::Packet(e) => Self::new(std::io::ErrorKind::InvalidData, e),
|
||||
Error::Timeout => Self::from(std::io::ErrorKind::TimedOut),
|
||||
Error::Unsupported => Self::from(std::io::ErrorKind::Unsupported),
|
||||
Error::Custom(_) => Self::other(err),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ impl Session {
|
|||
|
||||
async fn write(writer: &mut WriteHalf<ChannelStream<Msg>>, buf: Vec<u8>) -> io::Result<()> {
|
||||
if buf.is_empty() {
|
||||
Err(io::Error::new(ErrorKind::BrokenPipe, "channel closed"))
|
||||
Err(io::Error::from(ErrorKind::BrokenPipe))
|
||||
} else {
|
||||
writer.write_all(&buf).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ impl Handle {
|
|||
let mut b = 0;
|
||||
match unsafe { libc::read(self.inner, &mut b as *mut _ as *mut _, 1) } {
|
||||
-1 => Err(Error::last_os_error()),
|
||||
0 => Err(Error::new(ErrorKind::UnexpectedEof, "unexpected EOF")),
|
||||
0 => Err(Error::from(ErrorKind::UnexpectedEof)),
|
||||
_ => Ok(b),
|
||||
}
|
||||
}
|
||||
|
|
@ -189,7 +189,7 @@ impl Handle {
|
|||
if success == 0 {
|
||||
return Err(Error::last_os_error());
|
||||
} else if bytes == 0 {
|
||||
return Err(Error::new(ErrorKind::UnexpectedEof, "unexpected EOF"));
|
||||
return Err(Error::from(ErrorKind::UnexpectedEof));
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ impl Tty {
|
|||
let mut stdin = self.stdin.lock();
|
||||
loop {
|
||||
if now.elapsed() > timeout {
|
||||
return Err(Error::new(ErrorKind::TimedOut, "timed out"));
|
||||
return Err(Error::from(ErrorKind::TimedOut));
|
||||
} else if !stdin.poll(Duration::from_millis(30))? {
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ impl Watcher {
|
|||
}
|
||||
|
||||
pub fn watch<'a>(&mut self, it: impl Iterator<Item = &'a UrlBuf>) {
|
||||
self.in_tx.send(it.filter(|u| u.is_regular()).cloned().collect()).ok();
|
||||
self.in_tx.send(it.cloned().collect()).ok();
|
||||
}
|
||||
|
||||
pub fn push_files(&self, urls: Vec<UrlBuf>) { Backend::push_files(&self.out_tx, urls); }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue