feat: filesystem-level error (#3217)

This commit is contained in:
三咲雅 misaki masa 2025-09-28 23:08:48 +08:00 committed by GitHub
parent 7b36b7fe14
commit 879ed49996
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 265 additions and 81 deletions

2
Cargo.lock generated
View file

@ -4717,6 +4717,7 @@ dependencies = [
"ratatui",
"regex",
"serde",
"tokio",
"toml 0.9.7",
"tracing",
"yazi-codegen",
@ -5035,6 +5036,7 @@ dependencies = [
"toml 0.9.7",
"tracing",
"uzers",
"yazi-config",
"yazi-fs",
"yazi-macro",
"yazi-sftp",

View file

@ -56,7 +56,7 @@ impl UserData for Folder {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, cwd, |_, me| Ok(Url::new(me.url.to_owned())));
cached_field!(fields, files, |_, me| Files::make(0..me.files.len(), me, &me.tab));
cached_field!(fields, stage, |_, me| Ok(FolderStage::new(me.stage)));
cached_field!(fields, stage, |_, me| Ok(FolderStage::new(me.stage.clone())));
cached_field!(fields, window, |_, me| Files::make(me.window.clone(), me, &me.tab));
fields.add_field_method_get("offset", |_, me| Ok(me.offset));

View file

@ -46,7 +46,7 @@ impl Refresh {
match Files::from_dir_bulk(&cwd).await {
Ok(files) => FilesOp::Full(cwd, files, cha).emit(),
Err(e) => FilesOp::issue_error(&cwd, e.kind()).await,
Err(e) => FilesOp::issue_error(&cwd, e).await,
}
}

View file

@ -7,7 +7,7 @@ const EXPECTED: &str = "expected a Error";
pub enum Error {
Io(std::io::Error),
IoKind(std::io::ErrorKind),
Fs(yazi_fs::error::Error),
Serde(serde_json::Error),
Custom(SStr),
}
@ -24,7 +24,7 @@ impl Error {
pub fn into_string(self) -> SStr {
match self {
Self::Io(e) => Cow::Owned(e.to_string()),
Self::IoKind(e) => Cow::Owned(e.to_string()),
Self::Fs(e) => Cow::Owned(e.to_string()),
Self::Serde(e) => Cow::Owned(e.to_string()),
Self::Custom(s) => s,
}
@ -35,7 +35,7 @@ impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "{e}"),
Self::IoKind(e) => write!(f, "{e}"),
Self::Fs(e) => write!(f, "{e}"),
Self::Serde(e) => write!(f, "{e}"),
Self::Custom(s) => write!(f, "{s}"),
}
@ -56,6 +56,7 @@ impl UserData for Error {
fields.add_field_method_get("code", |_, me| {
Ok(match me {
Self::Io(e) => e.raw_os_error(),
Self::Fs(e) => e.raw_os_error(),
_ => None,
})
});
@ -64,7 +65,7 @@ impl UserData for Error {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(MetaMethod::ToString, |lua, me, ()| {
Ok(match me {
Self::Io(_) | Self::IoKind(_) | Self::Serde(_) => lua.create_string(me.to_string()),
Self::Io(_) | Self::Fs(_) | Self::Serde(_) => lua.create_string(me.to_string()),
Self::Custom(s) => lua.create_string(s.as_ref()),
})
});

View file

@ -11,10 +11,10 @@ impl UserData for FolderStage {
methods.add_meta_method(MetaMethod::Call, |lua, me, ()| {
use yazi_fs::FolderStage::*;
match me.0 {
match &me.0 {
Loading => false.into_lua_multi(lua),
Loaded => true.into_lua_multi(lua),
Failed(kind) => (true, crate::Error::IoKind(kind)).into_lua_multi(lua),
Failed(e) => (true, crate::Error::Fs(e.clone())).into_lua_multi(lua),
}
});
}

View file

@ -26,6 +26,7 @@ indexmap = { workspace = true }
ratatui = { workspace = true }
regex = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }

View file

@ -1,6 +1,6 @@
#![allow(clippy::module_inception)]
yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview tasks theme which);
yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview tasks theme which vfs);
yazi_macro::mod_flat!(color icon layout pattern platform preset priority style yazi);

View file

@ -56,7 +56,8 @@ impl Preview {
bail!("[preview].cache_dir must be a path within local filesystem.");
};
std::fs::create_dir_all(&self.cache_dir).context("Failed to create cache directory")?;
std::fs::create_dir_all(&self.cache_dir)
.context(format!("Failed to create cache directory: {}", self.cache_dir.display()))?;
Ok(self)
}

View file

@ -43,7 +43,7 @@ impl<T: Into<UrlBuf>> From<T> for Folder {
impl Folder {
pub fn update(&mut self, op: FilesOp) -> bool {
let (stage, revision) = (self.stage, self.files.revision);
let (stage, revision) = (self.stage.clone(), self.files.revision);
match op {
FilesOp::Full(_, _, cha) => {
(self.cha, self.stage) = (cha, FolderStage::Loaded);
@ -57,8 +57,8 @@ impl Folder {
FilesOp::Done(_, cha, ticket) if ticket == self.files.ticket() => {
(self.cha, self.stage) = (cha, FolderStage::Loaded);
}
FilesOp::IOErr(_, kind) => {
(self.cha, self.stage) = (Cha::default(), FolderStage::Failed(kind));
FilesOp::IOErr(_, ref err) => {
(self.cha, self.stage) = (Cha::default(), FolderStage::Failed(err.clone()));
}
_ => {}
}
@ -83,17 +83,15 @@ impl Folder {
self.trace = self.trace.take_if(|_| !self.files.is_empty() || self.stage.is_loading());
self.repos(None);
(stage, revision) != (self.stage, self.files.revision)
(&stage, revision) != (&self.stage, self.files.revision)
}
pub fn update_pub(&mut self, tab: Id, op: FilesOp) -> bool {
let old = self.stage;
if !self.update(op) {
return false;
} else if self.stage != old {
err!(Pubsub::pub_after_load(tab, &self.url, self.stage));
if self.update(op) {
err!(Pubsub::pub_after_load(tab, &self.url, &self.stage));
return true;
}
true
false
}
pub fn arrow(&mut self, step: impl Into<Step>) -> bool {

View file

@ -54,7 +54,7 @@ impl Preview {
let rx = match Files::from_dir(&wd).await {
Ok(rx) => rx,
Err(e) => return FilesOp::issue_error(&wd, e.kind()).await,
Err(e) => return FilesOp::issue_error(&wd, e).await,
};
let stream =

View file

@ -11,18 +11,18 @@ use super::Ember;
pub struct EmberLoad<'a> {
pub tab: Id,
pub url: Cow<'a, UrlBuf>,
pub stage: FolderStage,
pub stage: Cow<'a, FolderStage>,
}
impl<'a> EmberLoad<'a> {
pub fn borrowed(tab: Id, url: &'a UrlBuf, stage: FolderStage) -> Ember<'a> {
Self { tab, url: url.into(), stage }.into()
pub fn borrowed(tab: Id, url: &'a UrlBuf, stage: &'a FolderStage) -> Ember<'a> {
Self { tab, url: url.into(), stage: Cow::Borrowed(stage) }.into()
}
}
impl EmberLoad<'static> {
pub fn owned(tab: Id, url: &UrlBuf, stage: FolderStage) -> Ember<'static> {
Self { tab, url: url.clone().into(), stage }.into()
pub fn owned(tab: Id, url: &UrlBuf, stage: &FolderStage) -> Ember<'static> {
Self { tab, url: url.clone().into(), stage: Cow::Owned(stage.clone()) }.into()
}
}
@ -36,7 +36,7 @@ impl IntoLua for EmberLoad<'_> {
.create_table_from([
("tab", self.tab.get().into_lua(lua)?),
("url", yazi_binding::Url::new(self.url).into_lua(lua)?),
("stage", yazi_binding::FolderStage::new(self.stage).into_lua(lua)?),
("stage", yazi_binding::FolderStage::new(self.stage.into_owned()).into_lua(lua)?),
])?
.into_lua(lua)
}

View file

@ -149,7 +149,7 @@ impl Pubsub {
pub_after!(cd(tab: Id, url: &UrlBuf), (tab, url));
pub_after!(load(tab: Id, url: &UrlBuf, stage: FolderStage), (tab, url, stage));
pub_after!(load(tab: Id, url: &UrlBuf, stage: &FolderStage), (tab, url, stage));
pub_after!(hover(tab: Id, url: Option<&UrlBuf>), (tab, url));

View file

@ -0,0 +1,56 @@
use std::{fmt, io, sync::Arc};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Error {
Kind(io::ErrorKind),
Raw(i32),
Custom { kind: io::ErrorKind, code: Option<i32>, message: Arc<str> },
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Self {
if err.get_ref().is_some() {
Self::Custom {
kind: err.kind(),
code: err.raw_os_error(),
message: err.to_string().into(),
}
} else if let Some(code) = err.raw_os_error() {
Self::Raw(code)
} else {
Self::Kind(err.kind())
}
}
}
impl From<io::ErrorKind> for Error {
fn from(kind: io::ErrorKind) -> Self { Self::Kind(kind) }
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Kind(kind) => io::Error::from(*kind).fmt(f),
Self::Raw(code) => io::Error::from_raw_os_error(*code).fmt(f),
Self::Custom { message, .. } => write!(f, "{message}"),
}
}
}
impl Error {
pub fn kind(&self) -> io::ErrorKind {
match self {
Self::Kind(kind) => *kind,
Self::Raw(code) => io::Error::from_raw_os_error(*code).kind(),
Self::Custom { kind, .. } => *kind,
}
}
pub fn raw_os_error(&self) -> Option<i32> {
match self {
Self::Kind(_) => None,
Self::Raw(code) => Some(*code),
Self::Custom { code, .. } => *code,
}
}
}

1
yazi-fs/src/error/mod.rs Normal file
View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(error serde);

154
yazi-fs/src/error/serde.rs Normal file
View file

@ -0,0 +1,154 @@
use std::io;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::Error;
fn kind_to_str(kind: io::ErrorKind) -> &'static str {
use std::io::ErrorKind as K;
match kind {
K::NotFound => "not_found",
K::PermissionDenied => "permission_denied",
K::ConnectionRefused => "connection_refused",
K::ConnectionReset => "connection_reset",
K::HostUnreachable => "host_unreachable",
K::NetworkUnreachable => "network_unreachable",
K::ConnectionAborted => "connection_aborted",
K::NotConnected => "not_connected",
K::AddrInUse => "addr_in_use",
K::AddrNotAvailable => "addr_not_available",
K::NetworkDown => "network_down",
K::BrokenPipe => "broken_pipe",
K::AlreadyExists => "already_exists",
K::WouldBlock => "would_block",
K::NotADirectory => "not_a_directory",
K::IsADirectory => "is_a_directory",
K::DirectoryNotEmpty => "directory_not_empty",
K::ReadOnlyFilesystem => "read_only_filesystem",
// K::FilesystemLoop => "filesystem_loop",
K::StaleNetworkFileHandle => "stale_network_file_handle",
K::InvalidInput => "invalid_input",
K::InvalidData => "invalid_data",
K::TimedOut => "timed_out",
K::WriteZero => "write_zero",
K::StorageFull => "storage_full",
K::NotSeekable => "not_seekable",
K::QuotaExceeded => "quota_exceeded",
K::FileTooLarge => "file_too_large",
K::ResourceBusy => "resource_busy",
K::ExecutableFileBusy => "executable_file_busy",
K::Deadlock => "deadlock",
K::CrossesDevices => "crosses_devices",
K::TooManyLinks => "too_many_links",
K::InvalidFilename => "invalid_filename",
K::ArgumentListTooLong => "argument_list_too_long",
K::Interrupted => "interrupted",
K::Unsupported => "unsupported",
K::UnexpectedEof => "unexpected_eof",
K::OutOfMemory => "out_of_memory",
// K::InProgress => "in_progress",
K::Other => "other",
_ => "other",
}
}
fn kind_from_str(s: &str) -> io::ErrorKind {
use std::io::ErrorKind as K;
match s {
"not_found" => K::NotFound,
"permission_denied" => K::PermissionDenied,
"connection_refused" => K::ConnectionRefused,
"connection_reset" => K::ConnectionReset,
"host_unreachable" => K::HostUnreachable,
"network_unreachable" => K::NetworkUnreachable,
"connection_aborted" => K::ConnectionAborted,
"not_connected" => K::NotConnected,
"addr_in_use" => K::AddrInUse,
"addr_not_available" => K::AddrNotAvailable,
"network_down" => K::NetworkDown,
"broken_pipe" => K::BrokenPipe,
"already_exists" => K::AlreadyExists,
"would_block" => K::WouldBlock,
"not_a_directory" => K::NotADirectory,
"is_a_directory" => K::IsADirectory,
"directory_not_empty" => K::DirectoryNotEmpty,
"read_only_filesystem" => K::ReadOnlyFilesystem,
// "filesystem_loop" => K::FilesystemLoop,
"stale_network_file_handle" => K::StaleNetworkFileHandle,
"invalid_input" => K::InvalidInput,
"invalid_data" => K::InvalidData,
"timed_out" => K::TimedOut,
"write_zero" => K::WriteZero,
"storage_full" => K::StorageFull,
"not_seekable" => K::NotSeekable,
"quota_exceeded" => K::QuotaExceeded,
"file_too_large" => K::FileTooLarge,
"resource_busy" => K::ResourceBusy,
"executable_file_busy" => K::ExecutableFileBusy,
"deadlock" => K::Deadlock,
"crosses_devices" => K::CrossesDevices,
"too_many_links" => K::TooManyLinks,
"invalid_filename" => K::InvalidFilename,
"argument_list_too_long" => K::ArgumentListTooLong,
"interrupted" => K::Interrupted,
"unsupported" => K::Unsupported,
"unexpected_eof" => K::UnexpectedEof,
"out_of_memory" => K::OutOfMemory,
// "in_progress" => K::InProgress,
"other" => K::Other,
_ => K::Other,
}
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
#[derive(Serialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
enum Shadow<'a> {
Kind { kind: &'a str },
Raw { code: i32 },
Dyn { kind: &'a str, code: Option<i32>, message: &'a str },
}
match self {
Error::Kind(kind) => Shadow::Kind { kind: kind_to_str(*kind) }.serialize(serializer),
Error::Raw(code) => Shadow::Raw { code: *code }.serialize(serializer),
Error::Custom { kind, code, message } => {
Shadow::Dyn { kind: kind_to_str(*kind), code: *code, message }.serialize(serializer)
}
}
}
}
impl<'de> Deserialize<'de> for Error {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
enum Shadow {
Kind { kind: String },
Raw { code: i32 },
Dyn { kind: String, code: Option<i32>, message: String },
}
let shadow = Shadow::deserialize(deserializer)?;
Ok(match shadow {
Shadow::Kind { kind } => Error::Kind(kind_from_str(&kind)),
Shadow::Raw { code } => Error::Raw(code),
Shadow::Dyn { kind, code, message } => {
if !message.is_empty() {
Error::Custom { kind: kind_from_str(&kind), code, message: message.into() }
} else if let Some(code) = code {
Error::Raw(code)
} else {
Error::Kind(kind_from_str(&kind))
}
}
})
}
}

View file

@ -1,6 +1,6 @@
#![allow(clippy::if_same_then_else, clippy::option_map_unit_fn, clippy::unit_arg)]
yazi_macro::mod_pub!(cha mounts provider path);
yazi_macro::mod_pub!(cha error mounts path provider);
yazi_macro::mod_flat!(cwd file files filter fns op sorter sorting stage url xdg);

View file

@ -5,7 +5,7 @@ use yazi_macro::relay;
use yazi_shared::{Id, Ids, url::{UrlBuf, UrnBuf}};
use super::File;
use crate::cha::Cha;
use crate::{cha::Cha, error::Error};
pub static FILES_TICKET: Ids = Ids::new();
@ -15,7 +15,7 @@ pub enum FilesOp {
Part(UrlBuf, Vec<File>, Id),
Done(UrlBuf, Cha, Id),
Size(UrlBuf, HashMap<UrnBuf, u64>),
IOErr(UrlBuf, std::io::ErrorKind),
IOErr(UrlBuf, Error),
Creating(UrlBuf, Vec<File>),
Deleting(UrlBuf, HashSet<UrnBuf>),
@ -112,7 +112,7 @@ impl FilesOp {
Self::Part(_, files, ticket) => Self::Part(w, files!(files), *ticket),
Self::Done(_, cha, ticket) => Self::Done(w, *cha, *ticket),
Self::Size(_, map) => Self::Size(w, map.iter().map(|(urn, &s)| (urn.clone(), s)).collect()),
Self::IOErr(_, err) => Self::IOErr(w, *err),
Self::IOErr(_, err) => Self::IOErr(w, err.clone()),
Self::Creating(_, files) => Self::Creating(w, files!(files)),
Self::Deleting(_, urns) => Self::Deleting(w, urns.clone()),

View file

@ -1,42 +1,15 @@
use serde::{Deserialize, Serialize, ser::SerializeMap};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
use crate::error::Error;
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub enum FolderStage {
#[default]
Loading,
Loaded,
Failed(std::io::ErrorKind),
Failed(Error),
}
impl FolderStage {
pub fn is_loading(self) -> bool { self == Self::Loading }
}
impl Serialize for FolderStage {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(Some(2))?;
match self {
Self::Loading => map.serialize_entry("state", "loading")?,
Self::Loaded => map.serialize_entry("state", "loaded")?,
Self::Failed(_) => map.serialize_entry("state", "failed")?,
}
map.end()
}
}
impl<'de> Deserialize<'de> for FolderStage {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
struct Shadow {
state: String,
}
let shadow = Shadow::deserialize(deserializer)?;
match shadow.state.as_str() {
"loading" => Ok(Self::Loading),
"loaded" => Ok(Self::Loaded),
"failed" => Ok(Self::Failed(std::io::ErrorKind::Other)),
_ => Err(serde::de::Error::custom("invalid folder stage")),
}
}
pub fn is_loading(&self) -> bool { *self == Self::Loading }
}

View file

@ -9,6 +9,7 @@ homepage = "https://yazi-rs.github.io"
repository = "https://github.com/sxyazi/yazi"
[dependencies]
yazi-config = { path = "../yazi-config", version = "25.9.15" }
yazi-fs = { path = "../yazi-fs", version = "25.9.15" }
yazi-macro = { path = "../yazi-macro", version = "25.9.15" }
yazi-sftp = { path = "../yazi-sftp", version = "0.1.0" }

View file

@ -72,7 +72,7 @@ impl VfsFiles for Files {
Ok(c) if !c.is_dir() => FilesOp::issue_error(dir, ErrorKind::NotADirectory).await,
Ok(c) if c.hits(cha) && PARTITIONS.read().heuristic(cha) => {}
Ok(c) => return Some(c),
Err(e) => FilesOp::issue_error(dir, e.kind()).await,
Err(e) => FilesOp::issue_error(dir, e).await,
}
None
}

View file

@ -1,4 +1,4 @@
yazi_macro::mod_pub!(config provider);
yazi_macro::mod_pub!(provider);
yazi_macro::mod_flat!(cha file files fns op);

View file

@ -1,21 +1,19 @@
use std::io;
use yazi_fs::FilesOp;
use yazi_shared::url::UrlBuf;
use crate::maybe_exists;
pub trait VfsFilesOp {
fn issue_error(cwd: &UrlBuf, kind: io::ErrorKind) -> impl Future<Output = ()>;
fn issue_error(cwd: &UrlBuf, kind: impl Into<yazi_fs::error::Error>) -> impl Future<Output = ()>;
}
impl VfsFilesOp for FilesOp {
async fn issue_error(cwd: &UrlBuf, kind: std::io::ErrorKind) {
use std::io::ErrorKind;
if kind != ErrorKind::NotFound {
Self::IOErr(cwd.clone(), kind).emit();
async fn issue_error(cwd: &UrlBuf, err: impl Into<yazi_fs::error::Error>) {
let err = err.into();
if err.kind() != std::io::ErrorKind::NotFound {
Self::IOErr(cwd.clone(), err).emit();
} else if maybe_exists(cwd).await {
Self::IOErr(cwd.clone(), kind).emit();
Self::IOErr(cwd.clone(), err).emit();
} else if let Some((p, n)) = cwd.pair() {
Self::Deleting(p.into(), [n.into()].into()).emit();
}

View file

@ -1,10 +1,9 @@
use std::{io, path::{Path, PathBuf}, sync::Arc};
use yazi_config::vfs::{ProviderSftp, Vfs};
use yazi_fs::{cha::Cha, provider::{Provider, local::Local}};
use yazi_shared::{scheme::SchemeRef, url::{Url, UrlCow}};
use crate::config::{ProviderSftp, Vfs};
pub(super) struct Providers<'a>(Inner<'a>);
enum Inner<'a> {

View file

@ -1,11 +1,10 @@
use std::{io, path::Path};
use yazi_config::vfs::{ProviderSftp, Vfs};
use yazi_fs::{cha::Cha, provider::FileBuilder};
use yazi_sftp::fs::{Attrs, Flags};
use yazi_shared::scheme::SchemeRef;
use crate::config::{ProviderSftp, Vfs};
pub struct Gate {
sftp: super::Sftp,

View file

@ -3,7 +3,7 @@ yazi_macro::mod_flat!(gate metadata read_dir sftp);
pub(super) static CONN: yazi_shared::RoCell<
parking_lot::Mutex<
hashbrown::HashMap<
&'static crate::config::ProviderSftp,
&'static yazi_config::vfs::ProviderSftp,
&'static deadpool::managed::Pool<Sftp>,
>,
>,

View file

@ -2,12 +2,12 @@ use std::{io, path::{Path, PathBuf}, sync::Arc};
use russh::keys::PrivateKeyWithHashAlg;
use tokio::io::{BufReader, BufWriter};
use yazi_config::vfs::ProviderSftp;
use yazi_fs::provider::{DirReader, FileBuilder, FileHolder, Provider, local::Local};
use yazi_sftp::fs::{Attrs, Flags};
use yazi_shared::{scheme::SchemeRef, url::{Url, UrlBuf, UrlCow}};
use super::Cha;
use crate::config::ProviderSftp;
#[derive(Clone, Copy)]
pub struct Sftp {