feat: casefold support for the SFTP provider (#3201)

This commit is contained in:
三咲雅 misaki masa 2025-09-24 23:33:45 +08:00 committed by GitHub
parent 9f39363dd4
commit 6b09f2b665
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 95 additions and 32 deletions

View file

@ -1,17 +1,15 @@
use std::{io, path::{Path, PathBuf}}; 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 { pub async fn must_case_match(path: impl AsRef<Path>) -> bool {
let path = path.as_ref(); let path = path.as_ref();
casefold(path).await.is_ok_and(|p| p == path) 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( #[cfg(any(
target_os = "macos", target_os = "macos",
target_os = "netbsd", target_os = "netbsd",
@ -95,7 +93,7 @@ fn casefold_impl(path: PathBuf) -> io::Result<PathBuf> {
// Case-insensitive match // Case-insensitive match
Ok(entries.swap_remove(i)) Ok(entries.swap_remove(i))
} else { } else {
Err(io::Error::new(io::ErrorKind::NotFound, "file not found")) Err(io::Error::from(io::ErrorKind::NotFound))
} }
} }

View file

@ -32,6 +32,13 @@ impl Provider for Local {
tokio::fs::canonicalize(path).await tokio::fs::canonicalize(path).await
} }
async fn casefold<P>(&self, path: P) -> io::Result<PathBuf>
where
P: AsRef<Path>,
{
super::casefold(path).await
}
#[inline] #[inline]
async fn copy<P, Q>(&self, from: P, to: Q, cha: Cha) -> io::Result<u64> async fn copy<P, Q>(&self, from: P, to: Q, cha: Cha) -> io::Result<u64>
where where

View file

@ -67,11 +67,16 @@ pub async fn casefold<'a, U>(url: U) -> io::Result<UrlBuf>
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
if let Some(path) = url.into().as_path() { let url: Url = url.into();
local::casefold(path).await.map(Into::into) let fold = Providers::new(url).await?.casefold(url.loc).await?;
} else {
Err(io::Error::new(io::ErrorKind::Unsupported, "Unsupported filesystem")) 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> 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) { if original.scheme.covariant(link.scheme) {
Providers::new(original).await?.hard_link(original.loc, link.loc).await Providers::new(original).await?.hard_link(original.loc, link.loc).await
} else { } 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) { if from.scheme.covariant(to.scheme) {
Providers::new(from).await?.rename(from.loc, to.loc).await Providers::new(from).await?.rename(from.loc, to.loc).await
} else { } else {
Err(io::Error::new(io::ErrorKind::CrossesDevices, "Cross-filesystem rename")) Err(io::Error::from(io::ErrorKind::CrossesDevices))
} }
} }

View file

@ -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> async fn copy<P, Q>(&self, from: P, to: Q, cha: Cha) -> io::Result<u64>
where where
P: AsRef<Path>, P: AsRef<Path>,

View file

@ -2,10 +2,10 @@ use std::{ffi::OsStr, io, time::{Duration, UNIX_EPOCH}};
use crate::cha::{Cha, ChaKind, ChaMode}; 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; 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()))?; let mut cha = Self::try_from((ent.name().as_ref(), ent.attrs()))?;
cha.nlink = ent.nlink().unwrap_or_default(); cha.nlink = ent.nlink().unwrap_or_default();
Ok(cha) Ok(cha)

View file

@ -20,10 +20,7 @@ impl FileHolder for DirEntry {
fn name(&self) -> Cow<'_, OsStr> { self.0.name() } fn name(&self) -> Cow<'_, OsStr> { self.0.name() }
async fn metadata(&self) -> io::Result<Cha> { async fn metadata(&self) -> io::Result<Cha> { Cha::try_from(&self.0) }
let name = self.name();
(name.as_ref(), self.0.attrs()).try_into()
}
async fn file_type(&self) -> io::Result<ChaType> { async fn file_type(&self) -> io::Result<ChaType> {
ChaMode::try_from(self.0.attrs()).map(Into::into) ChaMode::try_from(self.0.attrs()).map(Into::into)

View file

@ -6,7 +6,7 @@ use yazi_sftp::fs::{Attrs, Flags};
use yazi_shared::{scheme::SchemeRef, url::{Url, UrlBuf, UrlCow}}; use yazi_shared::{scheme::SchemeRef, url::{Url, UrlBuf, UrlCow}};
use yazi_vfs::config::ProviderSftp; 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)] #[derive(Clone, Copy)]
pub struct Sftp { pub struct Sftp {
@ -44,6 +44,38 @@ impl Provider for Sftp {
Ok(self.op().await?.realpath(&path).await?) 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> async fn copy<P, Q>(&self, from: P, to: Q, cha: Cha) -> io::Result<u64>
where where
P: AsRef<Path>, P: AsRef<Path>,

View file

@ -19,6 +19,10 @@ pub trait Provider {
where where
P: AsRef<Path>; 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>> fn copy<P, Q>(&self, from: P, to: Q, cha: Cha) -> impl Future<Output = io::Result<u64>>
where where
P: AsRef<Path>, P: AsRef<Path>,

View file

@ -31,11 +31,21 @@ impl From<Error> for std::io::Error {
fn from(err: Error) -> Self { fn from(err: Error) -> Self {
match err { match err {
Error::IO(e) => e, Error::IO(e) => e,
Error::Serde(_) => Self::new(std::io::ErrorKind::InvalidData, err), Error::Serde(e) => Self::new(std::io::ErrorKind::InvalidData, e),
Error::Status(_) => Self::other(err), Error::Status(status) => match status.code {
Error::Packet(_) => Self::new(std::io::ErrorKind::InvalidData, err), responses::StatusCode::Ok => Self::new(std::io::ErrorKind::Other, "unexpected OK"),
Error::Timeout => Self::new(std::io::ErrorKind::TimedOut, err), responses::StatusCode::Eof => Self::from(std::io::ErrorKind::UnexpectedEof),
Error::Unsupported => Self::new(std::io::ErrorKind::Unsupported, err), 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), Error::Custom(_) => Self::other(err),
} }
} }

View file

@ -37,7 +37,7 @@ impl Session {
async fn write(writer: &mut WriteHalf<ChannelStream<Msg>>, buf: Vec<u8>) -> io::Result<()> { async fn write(writer: &mut WriteHalf<ChannelStream<Msg>>, buf: Vec<u8>) -> io::Result<()> {
if buf.is_empty() { if buf.is_empty() {
Err(io::Error::new(ErrorKind::BrokenPipe, "channel closed")) Err(io::Error::from(ErrorKind::BrokenPipe))
} else { } else {
writer.write_all(&buf).await writer.write_all(&buf).await
} }

View file

@ -120,7 +120,7 @@ impl Handle {
let mut b = 0; let mut b = 0;
match unsafe { libc::read(self.inner, &mut b as *mut _ as *mut _, 1) } { match unsafe { libc::read(self.inner, &mut b as *mut _ as *mut _, 1) } {
-1 => Err(Error::last_os_error()), -1 => Err(Error::last_os_error()),
0 => Err(Error::new(ErrorKind::UnexpectedEof, "unexpected EOF")), 0 => Err(Error::from(ErrorKind::UnexpectedEof)),
_ => Ok(b), _ => Ok(b),
} }
} }
@ -189,7 +189,7 @@ impl Handle {
if success == 0 { if success == 0 {
return Err(Error::last_os_error()); return Err(Error::last_os_error());
} else if bytes == 0 { } else if bytes == 0 {
return Err(Error::new(ErrorKind::UnexpectedEof, "unexpected EOF")); return Err(Error::from(ErrorKind::UnexpectedEof));
} }
Ok(buf) Ok(buf)
} }

View file

@ -38,7 +38,7 @@ impl Tty {
let mut stdin = self.stdin.lock(); let mut stdin = self.stdin.lock();
loop { loop {
if now.elapsed() > timeout { 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))? { } else if !stdin.poll(Duration::from_millis(30))? {
continue; continue;
} }

View file

@ -27,7 +27,7 @@ impl Watcher {
} }
pub fn watch<'a>(&mut self, it: impl Iterator<Item = &'a UrlBuf>) { 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); } pub fn push_files(&self, urls: Vec<UrlBuf>) { Backend::push_files(&self.out_tx, urls); }