feat: introduce ChaType for better cross-filesystem consistency (#3175)

This commit is contained in:
三咲雅 misaki masa 2025-09-18 07:20:19 +08:00 committed by GitHub
parent ece59974fe
commit 30c0279570
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 249 additions and 127 deletions

View file

@ -33,7 +33,7 @@ impl Actor for Rename {
.chars() .chars()
.rev() .rev()
.position(|c| c == '.') .position(|c| c == '.')
.filter(|_| !hovered.is_dir()) .filter(|_| hovered.is_file())
.map(|i| name.chars().count() - i - 1) .map(|i| name.chars().count() - i - 1)
.filter(|&i| i != 0), .filter(|&i| i != 0),
_ => None, _ => None,

View file

@ -28,8 +28,7 @@ impl Cha {
let kind = ChaKind::from_bits(t.raw_get("kind").unwrap_or_default()) let kind = ChaKind::from_bits(t.raw_get("kind").unwrap_or_default())
.ok_or_else(|| "Invalid kind".into_lua_err())?; .ok_or_else(|| "Invalid kind".into_lua_err())?;
let mode = let mode = ChaMode::try_from(t.raw_get::<u16>("mode")?)?;
ChaMode::from_bits(t.raw_get("mode")?).ok_or_else(|| "Invalid mode".into_lua_err())?;
Self(yazi_fs::cha::Cha { Self(yazi_fs::cha::Cha {
kind, kind,

View file

@ -1,11 +1,11 @@
use std::{ffi::OsStr, fs::{FileType, Metadata}, ops::Deref, time::{Duration, SystemTime, UNIX_EPOCH}}; use std::{ffi::OsStr, fs::Metadata, ops::Deref, time::{Duration, SystemTime, UNIX_EPOCH}};
use anyhow::bail; use anyhow::bail;
use yazi_macro::{unix_either, win_either}; use yazi_macro::{unix_either, win_either};
use yazi_shared::url::Url; use yazi_shared::url::Url;
use super::ChaKind; use super::ChaKind;
use crate::{cha::ChaMode, provider}; use crate::{cha::{ChaMode, ChaType}, provider};
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Cha { pub struct Cha {
@ -63,7 +63,7 @@ impl Cha {
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
let url: Url = url.into(); let url: Url = url.into();
let mut retain = cha.kind & (ChaKind::HIDDEN | ChaKind::SYSTEM | ChaKind::LINK); let mut retain = cha.kind & (ChaKind::HIDDEN | ChaKind::SYSTEM);
if cha.is_link() { if cha.is_link() {
cha = provider::metadata(url).await.unwrap_or(cha); cha = provider::metadata(url).await.unwrap_or(cha);
@ -75,16 +75,12 @@ impl Cha {
cha.attach(retain) cha.attach(retain)
} }
pub fn from_dummy<'a, U>(_url: U, ft: Option<FileType>) -> Self pub fn from_dummy<'a, U>(_url: U, r#type: Option<ChaType>) -> Self
where where
U: Into<Url<'a>>, U: Into<Url<'a>>,
{ {
let mode = ft.map(ChaMode::from_bare).unwrap_or_default();
let mut kind = ChaKind::DUMMY; let mut kind = ChaKind::DUMMY;
if mode.is_link() { let mode = r#type.map(ChaMode::from_bare).unwrap_or_default();
kind |= ChaKind::LINK;
}
#[cfg(unix)] #[cfg(unix)]
if _url.into().urn().is_hidden() { if _url.into().urn().is_hidden() {
@ -171,7 +167,7 @@ impl Cha {
if let Some(atime) = self.atime { if let Some(atime) = self.atime {
Ok(atime.duration_since(UNIX_EPOCH)?) Ok(atime.duration_since(UNIX_EPOCH)?)
} else { } else {
bail!("atime not supported on this platform"); bail!("atime not available");
} }
} }
@ -179,7 +175,7 @@ impl Cha {
if let Some(mtime) = self.mtime { if let Some(mtime) = self.mtime {
Ok(mtime.duration_since(UNIX_EPOCH)?) Ok(mtime.duration_since(UNIX_EPOCH)?)
} else { } else {
bail!("mtime not supported on this platform"); bail!("mtime not available");
} }
} }
@ -187,7 +183,7 @@ impl Cha {
if let Some(btime) = self.btime { if let Some(btime) = self.btime {
Ok(btime.duration_since(UNIX_EPOCH)?) Ok(btime.duration_since(UNIX_EPOCH)?)
} else { } else {
bail!("btime not supported on this platform"); bail!("btime not available");
} }
} }
@ -195,7 +191,7 @@ impl Cha {
if let Some(ctime) = self.ctime { if let Some(ctime) = self.ctime {
Ok(ctime.duration_since(UNIX_EPOCH)?) Ok(ctime.duration_since(UNIX_EPOCH)?)
} else { } else {
bail!("ctime not supported on this platform"); bail!("ctime not available");
} }
} }
} }

View file

@ -7,11 +7,8 @@ bitflags! {
pub struct ChaKind: u8 { pub struct ChaKind: u8 {
const HIDDEN = 0b0000_0001; const HIDDEN = 0b0000_0001;
const SYSTEM = 0b0000_0010; const SYSTEM = 0b0000_0010;
const ORPHAN = 0b0000_0100;
const LINK = 0b0000_0100; const DUMMY = 0b0000_1000;
const ORPHAN = 0b0000_1000;
const DUMMY = 0b0001_0000;
} }
} }

View file

@ -1 +1 @@
yazi_macro::mod_flat!(cha kind mode); yazi_macro::mod_flat!(cha kind mode r#type);

View file

@ -1,7 +1,10 @@
use std::fs::FileType; use std::ops::Deref;
use anyhow::{anyhow, bail};
use bitflags::bitflags; use bitflags::bitflags;
use crate::cha::ChaType;
bitflags! { bitflags! {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ChaMode: u16 { pub struct ChaMode: u16 {
@ -36,10 +39,43 @@ bitflags! {
} }
} }
impl ChaMode { impl Deref for ChaMode {
#[inline] type Target = ChaType;
pub fn r#type(self) -> Self { self & Self::T_MASK }
#[inline]
fn deref(&self) -> &Self::Target {
match *self & Self::T_MASK {
Self::T_FILE => &ChaType::File,
Self::T_DIR => &ChaType::Dir,
Self::T_LINK => &ChaType::Link,
Self::T_BLOCK => &ChaType::Block,
Self::T_CHAR => &ChaType::Char,
Self::T_SOCK => &ChaType::Sock,
Self::T_FIFO => &ChaType::FIFO,
_ => &ChaType::Unknown,
}
}
}
impl TryFrom<u16> for ChaMode {
type Error = anyhow::Error;
fn try_from(value: u16) -> Result<Self, Self::Error> {
let me = Self::from_bits(value).ok_or_else(|| anyhow!("invalid file mode: {value:04o}"))?;
match me & Self::T_MASK {
Self::T_FILE
| Self::T_DIR
| Self::T_LINK
| Self::T_BLOCK
| Self::T_CHAR
| Self::T_SOCK
| Self::T_FIFO => Ok(me),
_ => bail!("invalid file type: {value:04o}"),
}
}
}
impl ChaMode {
// Convert a file mode to a string representation // Convert a file mode to a string representation
#[cfg(unix)] #[cfg(unix)]
#[allow(clippy::collapsible_else_if)] #[allow(clippy::collapsible_else_if)]
@ -47,13 +83,13 @@ impl ChaMode {
let mut s = *b"-?????????"; let mut s = *b"-?????????";
// File type // File type
s[0] = match self.r#type() { s[0] = match *self {
Self::T_DIR => b'd', ChaType::Dir => b'd',
Self::T_LINK => b'l', ChaType::Link => b'l',
Self::T_BLOCK => b'b', ChaType::Block => b'b',
Self::T_CHAR => b'c', ChaType::Char => b'c',
Self::T_SOCK => b's', ChaType::Sock => b's',
Self::T_FIFO => b'p', ChaType::FIFO => b'p',
_ => b'-', _ => b'-',
}; };
if dummy { if dummy {
@ -90,69 +126,25 @@ impl ChaMode {
s s
} }
pub(super) fn from_bare(ft: FileType) -> Self { pub(super) fn from_bare(r#type: ChaType) -> Self {
#[cfg(unix)] match r#type {
{ ChaType::File => Self::T_FILE,
use std::os::unix::fs::FileTypeExt; ChaType::Dir => Self::T_DIR,
if ft.is_file() { ChaType::Link => Self::T_LINK,
Self::T_FILE ChaType::Block => Self::T_BLOCK,
} else if ft.is_dir() { ChaType::Char => Self::T_CHAR,
Self::T_DIR ChaType::Sock => Self::T_SOCK,
} else if ft.is_symlink() { ChaType::FIFO => Self::T_FIFO,
Self::T_LINK ChaType::Unknown => Self::empty(),
} else if ft.is_block_device() {
Self::T_BLOCK
} else if ft.is_char_device() {
Self::T_CHAR
} else if ft.is_socket() {
Self::T_SOCK
} else if ft.is_fifo() {
Self::T_FIFO
} else {
Self::empty()
}
}
#[cfg(windows)]
{
if ft.is_file() {
Self::T_FILE
} else if ft.is_dir() {
Self::T_DIR
} else if ft.is_symlink() {
Self::T_LINK
} else {
Self::empty()
}
} }
} }
} }
impl ChaMode { impl ChaMode {
#[inline]
pub const fn is_file(self) -> bool { self.contains(Self::T_FILE) }
#[inline]
pub const fn is_dir(self) -> bool { self.contains(Self::T_DIR) }
#[inline]
pub const fn is_link(&self) -> bool { self.contains(Self::T_LINK) }
#[inline]
pub const fn is_block(&self) -> bool { self.contains(Self::T_BLOCK) }
#[inline]
pub const fn is_char(&self) -> bool { self.contains(Self::T_CHAR) }
#[inline]
pub const fn is_sock(&self) -> bool { self.contains(Self::T_SOCK) }
#[inline]
pub const fn is_fifo(&self) -> bool { self.contains(Self::T_FIFO) }
// TODO: deprecate // TODO: deprecate
#[inline] #[inline]
pub const fn is_exec(&self) -> bool { self.contains(Self::U_EXEC) } pub const fn is_exec(self) -> bool { self.contains(Self::U_EXEC) }
#[inline] #[inline]
pub const fn is_sticky(&self) -> bool { self.contains(Self::S_STICKY) } pub const fn is_sticky(self) -> bool { self.contains(Self::S_STICKY) }
} }

80
yazi-fs/src/cha/type.rs Normal file
View file

@ -0,0 +1,80 @@
use std::fs::FileType;
use crate::cha::ChaMode;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ChaType {
File,
Dir,
Link,
Block,
Char,
Sock,
FIFO,
Unknown,
}
impl From<ChaMode> for ChaType {
fn from(value: ChaMode) -> Self { *value }
}
impl From<FileType> for ChaType {
fn from(value: FileType) -> Self {
#[cfg(unix)]
{
use std::os::unix::fs::FileTypeExt;
if value.is_file() {
Self::File
} else if value.is_dir() {
Self::Dir
} else if value.is_symlink() {
Self::Link
} else if value.is_block_device() {
Self::Block
} else if value.is_char_device() {
Self::Char
} else if value.is_socket() {
Self::Sock
} else if value.is_fifo() {
Self::FIFO
} else {
Self::Unknown
}
}
#[cfg(windows)]
{
if value.is_file() {
Self::File
} else if value.is_dir() {
Self::Dir
} else if value.is_symlink() {
Self::Link
} else {
Self::Unknown
}
}
}
}
impl ChaType {
#[inline]
pub fn is_file(self) -> bool { self == Self::File }
#[inline]
pub fn is_dir(self) -> bool { self == Self::Dir }
#[inline]
pub fn is_link(self) -> bool { self == Self::Link }
#[inline]
pub fn is_block(self) -> bool { self == Self::Block }
#[inline]
pub fn is_char(self) -> bool { self == Self::Char }
#[inline]
pub fn is_sock(self) -> bool { self == Self::Sock }
#[inline]
pub fn is_fifo(self) -> bool { self == Self::FIFO }
}

View file

@ -1,9 +1,9 @@
use std::{ffi::OsStr, fs::FileType, hash::{BuildHasher, Hash, Hasher}, ops::Deref, path::{Path, PathBuf}}; use std::{ffi::OsStr, hash::{BuildHasher, Hash, Hasher}, ops::Deref, path::{Path, PathBuf}};
use anyhow::Result; use anyhow::Result;
use yazi_shared::url::{Uri, UrlBuf, UrlCow, Urn}; use yazi_shared::url::{Uri, UrlBuf, UrlCow, Urn};
use crate::{cha::Cha, provider}; use crate::{cha::{Cha, ChaType}, provider};
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct File { pub struct File {
@ -36,9 +36,9 @@ impl File {
} }
#[inline] #[inline]
pub fn from_dummy(url: impl Into<UrlBuf>, ft: Option<FileType>) -> Self { pub fn from_dummy(url: impl Into<UrlBuf>, r#type: Option<ChaType>) -> Self {
let url = url.into(); let url = url.into();
let cha = Cha::from_dummy(&url, ft); let cha = Cha::from_dummy(&url, r#type);
Self { url, cha, link_to: None } Self { url, cha, link_to: None }
} }

View file

@ -40,14 +40,14 @@ impl Files {
let (tx, rx) = mpsc::unbounded_channel(); let (tx, rx) = mpsc::unbounded_channel();
tokio::spawn(async move { tokio::spawn(async move {
while let Ok(Some(item)) = it.next_entry().await { while let Ok(Some(ent)) = it.next_entry().await {
select! { select! {
_ = tx.closed() => break, _ = tx.closed() => break,
result = item.metadata() => { result = ent.metadata() => {
let url = item.url(); let url = ent.url();
_ = tx.send(match result { _ = tx.send(match result {
Ok(cha) => File::from_follow(url, cha).await, Ok(cha) => File::from_follow(url, cha).await,
Err(_) => File::from_dummy(url, item.file_type().await.ok()) Err(_) => File::from_dummy(url, ent.file_type().await.ok())
}); });
} }
} }
@ -67,11 +67,11 @@ impl Files {
let (second, third) = rest.split_at(entries.len() / 3); let (second, third) = rest.split_at(entries.len() / 3);
async fn go(entries: &[DirEntry]) -> Vec<File> { async fn go(entries: &[DirEntry]) -> Vec<File> {
let mut files = Vec::with_capacity(entries.len()); let mut files = Vec::with_capacity(entries.len());
for entry in entries { for ent in entries {
let url = entry.url(); let url = ent.url();
files.push(match entry.metadata().await { files.push(match ent.metadata().await {
Ok(cha) => File::from_follow(url, cha).await, Ok(cha) => File::from_follow(url, cha).await,
Err(_) => File::from_dummy(url, entry.file_type().await.ok()), Err(_) => File::from_dummy(url, ent.file_type().await.ok()),
}); });
} }
files files

View file

@ -83,9 +83,9 @@ pub fn copy_with_progress(
pub async fn remove_dir_clean(dir: &UrlBuf) { pub async fn remove_dir_clean(dir: &UrlBuf) {
let Ok(mut it) = provider::read_dir(dir).await else { return }; let Ok(mut it) = provider::read_dir(dir).await else { return };
while let Ok(Some(entry)) = it.next_entry().await { while let Ok(Some(ent)) = it.next_entry().await {
if entry.file_type().await.is_ok_and(|t| t.is_dir()) { if ent.file_type().await.is_ok_and(|t| t.is_dir()) {
let url = entry.url(); let url = ent.url();
Box::pin(remove_dir_clean(&url)).await; Box::pin(remove_dir_clean(&url)).await;
provider::remove_dir(&url).await.ok(); provider::remove_dir(&url).await.ok();
} }

View file

@ -2,7 +2,7 @@ use std::{borrow::Cow, ffi::OsStr, io};
use yazi_shared::url::UrlBuf; use yazi_shared::url::UrlBuf;
use crate::{cha::Cha, provider::FileHolder}; use crate::{cha::{Cha, ChaType}, provider::FileHolder};
pub enum DirEntry { pub enum DirEntry {
Local(super::local::DirEntry), Local(super::local::DirEntry),
@ -33,7 +33,7 @@ impl DirEntry {
} }
} }
pub async fn file_type(&self) -> io::Result<std::fs::FileType> { pub async fn file_type(&self) -> io::Result<ChaType> {
match self { match self {
Self::Local(local) => local.file_type().await, Self::Local(local) => local.file_type().await,
} }

View file

@ -1,6 +1,6 @@
use std::{borrow::Cow, ffi::OsStr, io, path::PathBuf}; use std::{borrow::Cow, ffi::OsStr, io, path::PathBuf};
use crate::{cha::Cha, provider::FileHolder}; use crate::{cha::{Cha, ChaType}, provider::FileHolder};
pub struct DirEntry(pub(super) tokio::fs::DirEntry); pub struct DirEntry(pub(super) tokio::fs::DirEntry);
@ -14,5 +14,5 @@ impl FileHolder for DirEntry {
Ok(Cha::new(&name, self.0.metadata().await?)) Ok(Cha::new(&name, self.0.metadata().await?))
} }
async fn file_type(&self) -> io::Result<std::fs::FileType> { self.0.file_type().await } async fn file_type(&self) -> io::Result<ChaType> { self.0.file_type().await.map(Into::into) }
} }

View file

@ -0,0 +1,59 @@
use std::{ffi::OsStr, io, time::{Duration, UNIX_EPOCH}};
use crate::cha::{Cha, ChaKind, ChaMode};
impl TryFrom<yazi_sftp::fs::DirEntry<'_>> for Cha {
type Error = io::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)
}
}
impl TryFrom<(&OsStr, &yazi_sftp::fs::Attrs)> for Cha {
type Error = io::Error;
fn try_from((name, attrs): (&OsStr, &yazi_sftp::fs::Attrs)) -> Result<Self, Self::Error> {
let kind =
if name.as_encoded_bytes().starts_with(b".") { ChaKind::HIDDEN } else { ChaKind::empty() };
Ok(Cha {
kind,
mode: attrs.try_into()?,
len: attrs.size.unwrap_or(0),
atime: attrs.atime.and_then(|t| UNIX_EPOCH.checked_add(Duration::from_secs(t as u64))),
btime: None,
ctime: None,
mtime: attrs.mtime.and_then(|t| UNIX_EPOCH.checked_add(Duration::from_secs(t as u64))),
dev: 0,
uid: attrs.uid.unwrap_or(0),
gid: attrs.gid.unwrap_or(0),
nlink: 0,
})
}
}
impl TryFrom<&yazi_sftp::fs::Attrs> for ChaMode {
type Error = io::Error;
fn try_from(attrs: &yazi_sftp::fs::Attrs) -> Result<Self, Self::Error> {
ChaMode::try_from(attrs.perm.unwrap_or_default() as u16)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
}
impl From<Cha> for yazi_sftp::fs::Attrs {
fn from(cha: Cha) -> Self {
Self {
size: Some(cha.len),
uid: Some(cha.uid),
gid: Some(cha.gid),
perm: Some(cha.mode.bits() as u32),
atime: cha.atime_dur().ok().map(|d| d.as_secs() as u32),
mtime: cha.mtime_dur().ok().map(|d| d.as_secs() as u32),
extended: Default::default(),
}
}
}

View file

@ -1,4 +1,4 @@
yazi_macro::mod_flat!(gate read_dir sftp); yazi_macro::mod_flat!(gate metadata read_dir sftp);
pub(super) static CONN: yazi_shared::RoCell<deadpool::managed::Pool<Sftp>> = pub(super) static CONN: yazi_shared::RoCell<deadpool::managed::Pool<Sftp>> =
yazi_shared::RoCell::new(); yazi_shared::RoCell::new();

View file

@ -1,6 +1,6 @@
use std::{borrow::Cow, ffi::OsStr, io, path::PathBuf}; use std::{borrow::Cow, ffi::OsStr, io, path::PathBuf};
use crate::{cha::Cha, provider::{DirReader, FileHolder}}; use crate::{cha::{Cha, ChaMode, ChaType}, provider::{DirReader, FileHolder}};
pub struct ReadDir(pub(super) yazi_sftp::fs::ReadDir); pub struct ReadDir(pub(super) yazi_sftp::fs::ReadDir);
@ -20,7 +20,12 @@ 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> { todo!() } async fn metadata(&self) -> io::Result<Cha> {
let name = self.name();
(name.as_ref(), self.0.attrs()).try_into()
}
async fn file_type(&self) -> io::Result<std::fs::FileType> { todo!() } async fn file_type(&self) -> io::Result<ChaType> {
ChaMode::try_from(self.0.attrs()).map(Into::into)
}
} }

View file

@ -30,19 +30,9 @@ impl Provider for Sftp {
P: AsRef<Path>, P: AsRef<Path>,
Q: AsRef<Path>, Q: AsRef<Path>,
{ {
// FIXME: pull this out to a From<Cha> for Attrs impl let attrs = Attrs::from(cha);
let attrs = Attrs {
size: Some(cha.len),
uid: Some(cha.uid),
gid: Some(cha.gid),
perm: Some(cha.mode.bits() as _),
atime: cha.atime_dur().ok().map(|d| d.as_secs() as u32),
mtime: cha.mtime_dur().ok().map(|d| d.as_secs() as u32),
extended: Default::default(),
};
let op = Self::op().await?; let op = Self::op().await?;
let mut from = op.open(&from, Flags::READ, Attrs::default()).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?; let mut to = op.open(&to, Flags::WRITE | Flags::CREATE | Flags::TRUNCATE, attrs).await?;
@ -68,7 +58,9 @@ impl Provider for Sftp {
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
todo!() let path = path.as_ref();
let attrs = Self::op().await?.stat(path).await?;
(path.file_name().unwrap_or_default(), &attrs).try_into()
} }
async fn read_dir<P>(path: P) -> io::Result<Self::ReadDir> async fn read_dir<P>(path: P) -> io::Result<Self::ReadDir>
@ -120,7 +112,9 @@ impl Provider for Sftp {
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
todo!() let path = path.as_ref();
let attrs = Self::op().await?.lstat(path).await?;
(path.file_name().unwrap_or_default(), &attrs).try_into()
} }
async fn trash<P>(_path: P) -> io::Result<()> async fn trash<P>(_path: P) -> io::Result<()>

View file

@ -3,7 +3,7 @@ use std::{borrow::Cow, ffi::OsStr, io, path::{Path, PathBuf}};
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
use yazi_macro::ok_or_not_found; use yazi_macro::ok_or_not_found;
use crate::cha::Cha; use crate::cha::{Cha, ChaType};
pub trait Provider { pub trait Provider {
type File: AsyncRead + AsyncWrite + Unpin; type File: AsyncRead + AsyncWrite + Unpin;
@ -189,7 +189,7 @@ pub trait FileHolder {
fn metadata(&self) -> impl Future<Output = io::Result<Cha>>; fn metadata(&self) -> impl Future<Output = io::Result<Cha>>;
fn file_type(&self) -> impl Future<Output = io::Result<std::fs::FileType>>; fn file_type(&self) -> impl Future<Output = io::Result<ChaType>>;
} }
// --- FileOpener // --- FileOpener