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()
.rev()
.position(|c| c == '.')
.filter(|_| !hovered.is_dir())
.filter(|_| hovered.is_file())
.map(|i| name.chars().count() - i - 1)
.filter(|&i| i != 0),
_ => None,

View file

@ -28,8 +28,7 @@ impl Cha {
let kind = ChaKind::from_bits(t.raw_get("kind").unwrap_or_default())
.ok_or_else(|| "Invalid kind".into_lua_err())?;
let mode =
ChaMode::from_bits(t.raw_get("mode")?).ok_or_else(|| "Invalid mode".into_lua_err())?;
let mode = ChaMode::try_from(t.raw_get::<u16>("mode")?)?;
Self(yazi_fs::cha::Cha {
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 yazi_macro::{unix_either, win_either};
use yazi_shared::url::Url;
use super::ChaKind;
use crate::{cha::ChaMode, provider};
use crate::{cha::{ChaMode, ChaType}, provider};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Cha {
@ -63,7 +63,7 @@ impl Cha {
U: Into<Url<'a>>,
{
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() {
cha = provider::metadata(url).await.unwrap_or(cha);
@ -75,16 +75,12 @@ impl Cha {
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
U: Into<Url<'a>>,
{
let mode = ft.map(ChaMode::from_bare).unwrap_or_default();
let mut kind = ChaKind::DUMMY;
if mode.is_link() {
kind |= ChaKind::LINK;
}
let mode = r#type.map(ChaMode::from_bare).unwrap_or_default();
#[cfg(unix)]
if _url.into().urn().is_hidden() {
@ -171,7 +167,7 @@ impl Cha {
if let Some(atime) = self.atime {
Ok(atime.duration_since(UNIX_EPOCH)?)
} else {
bail!("atime not supported on this platform");
bail!("atime not available");
}
}
@ -179,7 +175,7 @@ impl Cha {
if let Some(mtime) = self.mtime {
Ok(mtime.duration_since(UNIX_EPOCH)?)
} else {
bail!("mtime not supported on this platform");
bail!("mtime not available");
}
}
@ -187,7 +183,7 @@ impl Cha {
if let Some(btime) = self.btime {
Ok(btime.duration_since(UNIX_EPOCH)?)
} else {
bail!("btime not supported on this platform");
bail!("btime not available");
}
}
@ -195,7 +191,7 @@ impl Cha {
if let Some(ctime) = self.ctime {
Ok(ctime.duration_since(UNIX_EPOCH)?)
} else {
bail!("ctime not supported on this platform");
bail!("ctime not available");
}
}
}

View file

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

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 crate::cha::ChaType;
bitflags! {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ChaMode: u16 {
@ -36,10 +39,43 @@ bitflags! {
}
}
impl ChaMode {
#[inline]
pub fn r#type(self) -> Self { self & Self::T_MASK }
impl Deref for ChaMode {
type Target = ChaType;
#[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
#[cfg(unix)]
#[allow(clippy::collapsible_else_if)]
@ -47,13 +83,13 @@ impl ChaMode {
let mut s = *b"-?????????";
// File type
s[0] = match self.r#type() {
Self::T_DIR => b'd',
Self::T_LINK => b'l',
Self::T_BLOCK => b'b',
Self::T_CHAR => b'c',
Self::T_SOCK => b's',
Self::T_FIFO => b'p',
s[0] = match *self {
ChaType::Dir => b'd',
ChaType::Link => b'l',
ChaType::Block => b'b',
ChaType::Char => b'c',
ChaType::Sock => b's',
ChaType::FIFO => b'p',
_ => b'-',
};
if dummy {
@ -90,69 +126,25 @@ impl ChaMode {
s
}
pub(super) fn from_bare(ft: FileType) -> Self {
#[cfg(unix)]
{
use std::os::unix::fs::FileTypeExt;
if ft.is_file() {
Self::T_FILE
} else if ft.is_dir() {
Self::T_DIR
} else if ft.is_symlink() {
Self::T_LINK
} 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()
}
pub(super) fn from_bare(r#type: ChaType) -> Self {
match r#type {
ChaType::File => Self::T_FILE,
ChaType::Dir => Self::T_DIR,
ChaType::Link => Self::T_LINK,
ChaType::Block => Self::T_BLOCK,
ChaType::Char => Self::T_CHAR,
ChaType::Sock => Self::T_SOCK,
ChaType::FIFO => Self::T_FIFO,
ChaType::Unknown => Self::empty(),
}
}
}
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
#[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]
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 yazi_shared::url::{Uri, UrlBuf, UrlCow, Urn};
use crate::{cha::Cha, provider};
use crate::{cha::{Cha, ChaType}, provider};
#[derive(Clone, Debug, Default)]
pub struct File {
@ -36,9 +36,9 @@ impl File {
}
#[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 cha = Cha::from_dummy(&url, ft);
let cha = Cha::from_dummy(&url, r#type);
Self { url, cha, link_to: None }
}

View file

@ -40,14 +40,14 @@ impl Files {
let (tx, rx) = mpsc::unbounded_channel();
tokio::spawn(async move {
while let Ok(Some(item)) = it.next_entry().await {
while let Ok(Some(ent)) = it.next_entry().await {
select! {
_ = tx.closed() => break,
result = item.metadata() => {
let url = item.url();
result = ent.metadata() => {
let url = ent.url();
_ = tx.send(match result {
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);
async fn go(entries: &[DirEntry]) -> Vec<File> {
let mut files = Vec::with_capacity(entries.len());
for entry in entries {
let url = entry.url();
files.push(match entry.metadata().await {
for ent in entries {
let url = ent.url();
files.push(match ent.metadata().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

View file

@ -83,9 +83,9 @@ pub fn copy_with_progress(
pub async fn remove_dir_clean(dir: &UrlBuf) {
let Ok(mut it) = provider::read_dir(dir).await else { return };
while let Ok(Some(entry)) = it.next_entry().await {
if entry.file_type().await.is_ok_and(|t| t.is_dir()) {
let url = entry.url();
while let Ok(Some(ent)) = it.next_entry().await {
if ent.file_type().await.is_ok_and(|t| t.is_dir()) {
let url = ent.url();
Box::pin(remove_dir_clean(&url)).await;
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 crate::{cha::Cha, provider::FileHolder};
use crate::{cha::{Cha, ChaType}, provider::FileHolder};
pub enum 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 {
Self::Local(local) => local.file_type().await,
}

View file

@ -1,6 +1,6 @@
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);
@ -14,5 +14,5 @@ impl FileHolder for DirEntry {
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>> =
yazi_shared::RoCell::new();

View file

@ -1,6 +1,6 @@
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);
@ -20,7 +20,12 @@ impl FileHolder for DirEntry<'_> {
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>,
Q: AsRef<Path>,
{
// 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_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 attrs = Attrs::from(cha);
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?;
@ -68,7 +58,9 @@ impl Provider for Sftp {
where
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>
@ -120,7 +112,9 @@ impl Provider for Sftp {
where
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<()>

View file

@ -3,7 +3,7 @@ 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;
use crate::cha::{Cha, ChaType};
pub trait Provider {
type File: AsyncRead + AsyncWrite + Unpin;
@ -189,7 +189,7 @@ pub trait FileHolder {
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