refactor: pull filesystem calls out into a designated yazi_fs crate (#3013)

This commit is contained in:
三咲雅 misaki masa 2025-07-23 22:43:11 +08:00 committed by GitHub
parent 1f596a01fc
commit c2883f1e05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 354 additions and 145 deletions

1
Cargo.lock generated
View file

@ -3530,6 +3530,7 @@ dependencies = [
"tokio",
"tracing",
"yazi-config",
"yazi-fs",
"yazi-macro",
"yazi-shared",
"yazi-term",

View file

@ -1,8 +1,7 @@
use std::{ffi::OsString, mem, path::{MAIN_SEPARATOR_STR, Path, PathBuf}};
use anyhow::Result;
use tokio::fs;
use yazi_fs::{CWD, expand_path};
use yazi_fs::{CWD, expand_path, services::Local};
use yazi_macro::{act, render, succ};
use yazi_parser::cmp::{CmpItem, ShowOpt, TriggerOpt};
use yazi_proxy::CmpProxy;
@ -37,7 +36,8 @@ impl Actor for Trigger {
let ticket = cmp.ticket;
tokio::spawn(async move {
let mut dir = fs::read_dir(&parent).await?;
// TODO: support VFS
let mut dir = Local::read_dir(&parent).await?;
let mut cache = vec![];
// "/" is both a directory separator and the root directory per se

View file

@ -22,6 +22,7 @@ impl Actor for BulkRename {
const NAME: &str = "bulk_rename";
// FIXME: VFS
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let Some(opener) = YAZI.opener.block(YAZI.open.all("bulk-rename.txt", "text/plain")) else {
succ!(AppProxy::notify_warn("Bulk rename", "No text opener found"));

View file

@ -1,7 +1,6 @@
use anyhow::Result;
use tokio::fs;
use yazi_config::popup::{ConfirmCfg, InputCfg};
use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, realname};
use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, realname, services};
use yazi_macro::succ;
use yazi_parser::mgr::CreateOpt;
use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER};
@ -46,15 +45,15 @@ impl Create {
let _permit = WATCHER.acquire().await.unwrap();
if dir {
fs::create_dir_all(&new).await?;
services::create_dir_all(&new).await?;
} else if let Some(real) = realname(&new).await {
ok_or_not_found(fs::remove_file(&new).await)?;
ok_or_not_found(services::remove_file(&new).await)?;
FilesOp::Deleting(parent.clone(), [UrnBuf::from(real)].into()).emit();
fs::File::create(&new).await?;
services::create(&new).await?;
} else {
fs::create_dir_all(&parent).await.ok();
ok_or_not_found(fs::remove_file(&new).await)?;
fs::File::create(&new).await?;
services::create_dir_all(&parent).await.ok();
ok_or_not_found(services::remove_file(&new).await)?;
services::create(&new).await?;
}
if let Ok(f) = File::new(new.clone()).await {

View file

@ -1,8 +1,7 @@
use anyhow::Result;
use tokio::fs;
use yazi_config::popup::{ConfirmCfg, InputCfg};
use yazi_dds::Pubsub;
use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, paths_to_same_file, realname};
use yazi_fs::{File, FilesOp, maybe_exists, ok_or_not_found, paths_to_same_file, realname, services};
use yazi_macro::{act, err, succ};
use yazi_parser::mgr::RenameOpt;
use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy, WATCHER};
@ -66,10 +65,10 @@ impl Rename {
let _permit = WATCHER.acquire().await.unwrap();
let overwritten = realname(&new).await;
fs::rename(&old, &new).await?;
services::rename(&old, &new).await?;
if let Some(o) = overwritten {
ok_or_not_found(fs::rename(p_new.join(&o), &new).await)?;
ok_or_not_found(services::rename(p_new.join(&o), &new).await)?;
FilesOp::Deleting(p_new.clone(), [UrnBuf::from(o)].into()).emit();
}

View file

@ -10,6 +10,7 @@ repository = "https://github.com/sxyazi/yazi"
[dependencies]
yazi-config = { path = "../yazi-config", version = "25.6.11" }
yazi-fs = { path = "../yazi-fs", version = "25.6.11" }
yazi-macro = { path = "../yazi-macro", version = "25.6.11" }
yazi-shared = { path = "../yazi-shared", version = "25.6.11" }
yazi-term = { path = "../yazi-term", version = "25.6.11" }

View file

@ -1,9 +1,9 @@
use std::{env, fmt::Display, path::Path};
use std::{env, fmt::Display};
use anyhow::Result;
use ratatui::layout::Rect;
use tracing::warn;
use yazi_shared::env_exists;
use yazi_shared::{env_exists, url::Url};
use crate::{Emulator, SHOWN, TMUX, drivers};
@ -35,18 +35,18 @@ impl Display for Adapter {
}
impl Adapter {
pub async fn image_show(self, path: &Path, max: Rect) -> Result<Rect> {
pub async fn image_show(self, url: &Url, max: Rect) -> Result<Rect> {
if max.is_empty() {
return Ok(Rect::default());
}
match self {
Self::Kgp => drivers::Kgp::image_show(path, max).await,
Self::KgpOld => drivers::KgpOld::image_show(path, max).await,
Self::Iip => drivers::Iip::image_show(path, max).await,
Self::Sixel => drivers::Sixel::image_show(path, max).await,
Self::X11 | Self::Wayland => drivers::Ueberzug::image_show(path, max).await,
Self::Chafa => drivers::Chafa::image_show(path, max).await,
Self::Kgp => drivers::Kgp::image_show(url, max).await,
Self::KgpOld => drivers::KgpOld::image_show(url, max).await,
Self::Iip => drivers::Iip::image_show(url, max).await,
Self::Sixel => drivers::Sixel::image_show(url, max).await,
Self::X11 | Self::Wayland => drivers::Ueberzug::image_show(url, max).await,
Self::Chafa => drivers::Chafa::image_show(url, max).await,
}
}

View file

@ -1,4 +1,4 @@
use std::{fmt::Write, io::Write as ioWrite, path::Path};
use std::{fmt::Write, io::Write as ioWrite};
use anyhow::Result;
use base64::{Engine, engine::{Config, general_purpose::STANDARD}};
@ -6,14 +6,15 @@ use crossterm::{cursor::MoveTo, queue};
use image::{DynamicImage, ExtendedColorType, ImageEncoder, codecs::{jpeg::JpegEncoder, png::PngEncoder}};
use ratatui::layout::Rect;
use yazi_config::YAZI;
use yazi_shared::url::Url;
use crate::{CLOSE, Emulator, Image, START, adapter::Adapter};
pub(crate) struct Iip;
impl Iip {
pub(crate) async fn image_show(path: &Path, max: Rect) -> Result<Rect> {
let img = Image::downscale(path, max).await?;
pub(crate) async fn image_show(url: &Url, max: Rect) -> Result<Rect> {
let img = Image::downscale(url, max).await?;
let area = Image::pixel_area((img.width(), img.height()), max);
let b = Self::encode(img).await?;

View file

@ -1,11 +1,12 @@
use core::str;
use std::{io::Write, path::Path};
use std::io::Write;
use anyhow::Result;
use base64::{Engine, engine::general_purpose};
use crossterm::{cursor::MoveTo, queue};
use image::DynamicImage;
use ratatui::layout::Rect;
use yazi_shared::url::Url;
use crate::{CLOSE, ESCAPE, Emulator, START, adapter::Adapter, image::Image};
@ -312,8 +313,8 @@ static DIACRITICS: [char; 297] = [
pub(crate) struct Kgp;
impl Kgp {
pub(crate) async fn image_show(path: &Path, max: Rect) -> Result<Rect> {
let img = Image::downscale(path, max).await?;
pub(crate) async fn image_show(url: &Url, max: Rect) -> Result<Rect> {
let img = Image::downscale(url, max).await?;
let area = Image::pixel_area((img.width(), img.height()), max);
let b1 = Self::encode(img).await?;

View file

@ -1,10 +1,11 @@
use core::str;
use std::{io::Write, path::Path};
use std::io::Write;
use anyhow::Result;
use base64::{Engine, engine::general_purpose};
use image::DynamicImage;
use ratatui::layout::Rect;
use yazi_shared::url::Url;
use yazi_term::tty::TTY;
use crate::{CLOSE, ESCAPE, Emulator, Image, START, adapter::Adapter};
@ -12,8 +13,8 @@ use crate::{CLOSE, ESCAPE, Emulator, Image, START, adapter::Adapter};
pub(crate) struct KgpOld;
impl KgpOld {
pub(crate) async fn image_show(path: &Path, max: Rect) -> Result<Rect> {
let img = Image::downscale(path, max).await?;
pub(crate) async fn image_show(url: &Url, max: Rect) -> Result<Rect> {
let img = Image::downscale(url, max).await?;
let area = Image::pixel_area((img.width(), img.height()), max);
let b = Self::encode(img).await?;

View file

@ -1,4 +1,4 @@
use std::{io::Write, path::Path};
use std::io::Write;
use anyhow::{Result, bail};
use crossterm::{cursor::MoveTo, queue};
@ -6,14 +6,15 @@ use image::{DynamicImage, GenericImageView, RgbImage};
use palette::{Srgb, cast::ComponentsAs};
use quantette::{ColorSlice, PaletteSize, QuantizeOutput, wu::UIntBinner};
use ratatui::layout::Rect;
use yazi_shared::url::Url;
use crate::{CLOSE, ESCAPE, Emulator, Image, START, adapter::Adapter};
pub(crate) struct Sixel;
impl Sixel {
pub(crate) async fn image_show(path: &Path, max: Rect) -> Result<Rect> {
let img = Image::downscale(path, max).await?;
pub(crate) async fn image_show(url: &Url, max: Rect) -> Result<Rect> {
let img = Image::downscale(url, max).await?;
let area = Image::pixel_area((img.width(), img.height()), max);
let b = Self::encode(img).await?;

View file

@ -1,17 +1,19 @@
use std::path::Path;
use std::io::BufReader;
use anyhow::Result;
use image::{DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation};
use image::{DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageFormat, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation};
use ratatui::layout::Rect;
use yazi_config::YAZI;
use yazi_fs::services;
use yazi_shared::url::Url;
use crate::Dimension;
pub struct Image;
impl Image {
pub async fn precache(path: &Path, cache: &Path) -> Result<()> {
let (mut img, orientation, icc) = Self::decode_from(path).await?;
pub async fn precache(src: &Url, cache: &Url) -> Result<()> {
let (mut img, orientation, icc) = Self::decode_from(src).await?;
let (w, h) = Self::flip_size(orientation, (YAZI.preview.max_width, YAZI.preview.max_height));
let buf = tokio::task::spawn_blocking(move || {
@ -38,11 +40,11 @@ impl Image {
})
.await??;
Ok(tokio::fs::write(cache, buf).await?)
Ok(services::write(cache, buf).await?)
}
pub(super) async fn downscale(path: &Path, rect: Rect) -> Result<DynamicImage> {
let (mut img, orientation, _) = Self::decode_from(path).await?;
pub(super) async fn downscale(url: &Url, rect: Rect) -> Result<DynamicImage> {
let (mut img, orientation, _) = Self::decode_from(url).await?;
let (w, h) = Self::flip_size(orientation, Self::max_pixel(rect));
// Fast path.
@ -96,7 +98,7 @@ impl Image {
}
}
async fn decode_from(path: &Path) -> ImageResult<(DynamicImage, Orientation, Option<Vec<u8>>)> {
async fn decode_from(url: &Url) -> ImageResult<(DynamicImage, Orientation, Option<Vec<u8>>)> {
let mut limits = Limits::no_limits();
if YAZI.tasks.image_alloc > 0 {
limits.max_alloc = Some(YAZI.tasks.image_alloc as u64);
@ -108,11 +110,13 @@ impl Image {
limits.max_image_height = Some(YAZI.tasks.image_bound[1] as u32);
}
let path = path.to_owned();
tokio::task::spawn_blocking(move || {
let mut reader = ImageReader::open(path)?;
reader.limits(limits);
let mut reader = ImageReader::new(BufReader::new(services::open(&url).await?.into_std().await));
if let Ok(format) = ImageFormat::from_path(url) {
reader.set_format(format);
}
reader.limits(limits);
tokio::task::spawn_blocking(move || {
let mut decoder = reader.with_guessed_format()?.into_decoder()?;
let orientation = decoder.orientation().unwrap_or(Orientation::NoTransforms);
let icc = decoder.icc_profile().unwrap_or_default();

View file

@ -25,6 +25,11 @@ impl Deref for Url {
fn deref(&self) -> &Self::Target { &self.inner }
}
impl AsRef<yazi_shared::url::Url> for Url {
fn as_ref(&self) -> &yazi_shared::url::Url { &self.inner }
}
// FIXME: remove
impl AsRef<Path> for Url {
fn as_ref(&self) -> &Path { self.inner.as_path() }
}

View file

@ -1,6 +1,5 @@
use anyhow::{Context, Result};
use tokio::fs;
use yazi_fs::{maybe_exists, ok_or_not_found, remove_dir_clean, remove_sealed};
use yazi_fs::{maybe_exists, ok_or_not_found, remove_dir_clean, remove_sealed, services::Local};
use yazi_macro::outln;
use super::Dependency;
@ -23,7 +22,7 @@ impl Dependency {
pub(super) async fn delete_assets(&self) -> Result<()> {
let assets = self.target().join("assets");
match fs::read_dir(&assets).await {
match Local::read_dir(&assets).await {
Ok(mut it) => {
while let Some(entry) = it.next_entry().await? {
remove_sealed(&entry.path())
@ -52,7 +51,7 @@ impl Dependency {
.with_context(|| format!("failed to delete `{}`", p.display()))?;
}
if ok_or_not_found(fs::remove_dir(&dir).await).is_ok() {
if ok_or_not_found(Local::remove_dir(&dir).await).is_ok() {
outln!("Done!")?;
} else {
outln!(

View file

@ -1,8 +1,7 @@
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use tokio::fs;
use yazi_fs::{copy_and_seal, maybe_exists, remove_dir_clean};
use yazi_fs::{copy_and_seal, maybe_exists, remove_dir_clean, services::Local};
use yazi_macro::outln;
use super::Dependency;
@ -20,7 +19,7 @@ impl Dependency {
self.hash_check().await?;
}
fs::create_dir_all(&to).await?;
Local::create_dir_all(&to).await?;
self.delete_assets().await?;
let res1 = Self::deploy_assets(from.join("assets"), to.join("assets")).await;
@ -40,9 +39,9 @@ impl Dependency {
}
async fn deploy_assets(from: PathBuf, to: PathBuf) -> Result<()> {
match fs::read_dir(&from).await {
match Local::read_dir(&from).await {
Ok(mut it) => {
fs::create_dir_all(&to).await?;
Local::create_dir_all(&to).await?;
while let Some(entry) = it.next_entry().await? {
let (src, dist) = (entry.path(), to.join(entry.file_name()));
copy_and_seal(&src, &dist).await.with_context(|| {

View file

@ -1,7 +1,6 @@
use anyhow::{Context, Result, bail};
use tokio::fs;
use twox_hash::XxHash3_128;
use yazi_fs::ok_or_not_found;
use yazi_fs::{ok_or_not_found, services::Local};
use super::Dependency;
@ -26,17 +25,17 @@ impl Dependency {
for &file in files {
h.write(file.as_bytes());
h.write(b"VpvFw9Atb7cWGOdqhZCra634CcJJRlsRl72RbZeV0vpG1\0");
h.write(&ok_or_not_found(fs::read(dir.join(file)).await)?);
h.write(&ok_or_not_found(Local::read(dir.join(file)).await)?);
}
let mut assets = vec![];
match fs::read_dir(dir.join("assets")).await {
match Local::read_dir(dir.join("assets")).await {
Ok(mut it) => {
while let Some(entry) = it.next_entry().await? {
let Ok(name) = entry.file_name().into_string() else {
bail!("asset path is not valid UTF-8: {}", entry.path().display());
};
assets.push((name, fs::read(entry.path()).await?));
assets.push((name, Local::read(entry.path()).await?));
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}

View file

@ -2,8 +2,7 @@ use std::{path::PathBuf, str::FromStr};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use tokio::fs;
use yazi_fs::Xdg;
use yazi_fs::{Xdg, services::Local};
use yazi_macro::outln;
use super::Dependency;
@ -16,7 +15,7 @@ pub(crate) struct Package {
impl Package {
pub(crate) async fn load() -> Result<Self> {
Ok(match fs::read_to_string(Self::toml()).await {
Ok(match Local::read_to_string(Self::toml()).await {
Ok(s) => toml::from_str(&s)?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Self::default(),
Err(e) => Err(e)?,
@ -136,7 +135,7 @@ impl Package {
async fn save(&self) -> Result<()> {
let s = toml::to_string_pretty(self)?;
fs::write(Self::toml(), s).await.context("Failed to write package.toml")
Local::write(Self::toml(), s).await.context("Failed to write package.toml")
}
#[inline]

View file

@ -3,10 +3,10 @@ use std::{collections::{HashMap, HashSet}, time::Duration};
use anyhow::Result;
use notify::{PollWatcher, RecommendedWatcher, RecursiveMode, Watcher as _Watcher};
use parking_lot::RwLock;
use tokio::{fs, pin, sync::{mpsc::{self, UnboundedReceiver}, watch}};
use tokio::{pin, sync::{mpsc::{self, UnboundedReceiver}, watch}};
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
use tracing::error;
use yazi_fs::{File, Files, FilesOp, cha::Cha, realname_unchecked};
use yazi_fs::{File, Files, FilesOp, cha::Cha, realname_unchecked, services};
use yazi_proxy::WATCHER;
use yazi_shared::{RoCell, url::Url};
@ -21,6 +21,7 @@ pub struct Watcher {
out_tx: mpsc::UnboundedSender<Url>,
}
// FIXME: VFS
impl Watcher {
pub(super) fn serve() -> Self {
let (in_tx, in_rx) = watch::channel(Default::default());
@ -138,7 +139,7 @@ impl Watcher {
};
let u = &file.url;
let eq = (!file.is_link() && fs::canonicalize(u).await.is_ok_and(|p| p == ***u))
let eq = (!file.is_link() && services::canonicalize(u).await.is_ok_and(|c| c == *u))
|| realname_unchecked(u, &mut cached).await.is_ok_and(|s| urn.as_urn() == s);
if !eq {
@ -193,10 +194,10 @@ impl Watcher {
async fn go(todo: HashSet<Url>) {
for from in todo {
let Ok(to) = fs::canonicalize(&from).await else { continue };
let Ok(to) = services::canonicalize(&from).await else { continue };
if to != **from && WATCHED.read().contains(&from) {
LINKED.write().insert(from, Url::from(to));
if to != from && WATCHED.read().contains(&from) {
LINKED.write().insert(from, to);
}
}
}

View file

@ -2,8 +2,9 @@ use std::{collections::HashMap, mem, ops::Deref, sync::atomic::{AtomicU64, Order
use anyhow::Result;
use parking_lot::RwLock;
use tokio::{fs::{self, File, OpenOptions}, io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter}};
use tokio::{fs::OpenOptions, io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter}};
use yazi_boot::BOOT;
use yazi_fs::services::Local;
use yazi_shared::{RoCell, timestamp_us};
use crate::CLIENTS;
@ -56,8 +57,9 @@ impl State {
return Ok(());
}
fs::create_dir_all(&BOOT.state_dir).await?;
Local::create_dir_all(&BOOT.state_dir).await?;
let mut buf = BufWriter::new(
// TODO: VFS
OpenOptions::new()
.write(true)
.create(true)
@ -77,7 +79,7 @@ impl State {
}
async fn load(&self) -> Result<()> {
let mut file = BufReader::new(File::open(BOOT.state_dir.join(".dds")).await?);
let mut file = BufReader::new(Local::open(BOOT.state_dir.join(".dds")).await?);
let mut buf = String::new();
let mut inner = HashMap::new();
@ -101,7 +103,7 @@ impl State {
}
async fn skip(&self) -> Result<bool> {
let meta = fs::symlink_metadata(BOOT.state_dir.join(".dds")).await?;
let meta = Local::symlink_metadata(BOOT.state_dir.join(".dds")).await?;
let modified = meta.modified()?.duration_since(UNIX_EPOCH)?.as_micros();
Ok(modified >= self.last.load(Ordering::Relaxed) as u128)
}

View file

@ -38,7 +38,7 @@ impl Stream {
pub(super) async fn bind() -> std::io::Result<ServerListener> {
let p = Self::socket_file();
tokio::fs::remove_file(&p).await.ok();
yazi_fs::services::Local::remove_file(&p).await.ok();
tokio::net::UnixListener::bind(p)
}

View file

@ -1,7 +1,7 @@
use std::ffi::OsString;
use tokio::fs;
use yazi_boot::ARGS;
use yazi_fs::services::Local;
use yazi_shared::event::EventQuit;
use crate::{Term, app::App};
@ -26,13 +26,13 @@ impl App {
async fn cwd_to_file(&self, no: bool) {
if let Some(p) = ARGS.cwd_file.as_ref().filter(|_| !no) {
let cwd = self.core.mgr.cwd().as_os_str();
fs::write(p, cwd.as_encoded_bytes()).await.ok();
Local::write(p, cwd.as_encoded_bytes()).await.ok();
}
}
async fn selected_to_file(&self, selected: Option<OsString>) {
if let (Some(s), Some(p)) = (selected, &ARGS.chooser_file) {
fs::write(p, s.as_encoded_bytes()).await.ok();
Local::write(p, s.as_encoded_bytes()).await.ok();
}
}
}

View file

@ -1,10 +1,10 @@
use std::{fs::{FileType, Metadata}, path::Path, time::SystemTime};
use std::{fs::{FileType, Metadata}, time::SystemTime};
use tokio::fs;
use yazi_macro::{unix_either, win_either};
use yazi_shared::url::Url;
use super::ChaKind;
use crate::services;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Cha {
@ -53,20 +53,20 @@ impl Default for Cha {
impl Cha {
#[inline]
pub fn new(path: &Path, meta: Metadata) -> Self {
Self::from_just_meta(&meta).attach(ChaKind::hidden(path, &meta))
pub fn new(url: &Url, meta: Metadata) -> Self {
Self::from_just_meta(&meta).attach(ChaKind::hidden(url, &meta))
}
#[inline]
pub async fn from_url(url: &Url) -> std::io::Result<Self> {
Ok(Self::from_follow(url, fs::symlink_metadata(url).await?).await)
Ok(Self::from_follow(url, services::symlink_metadata(url).await?).await)
}
pub async fn from_follow(path: &Path, mut meta: Metadata) -> Self {
let mut attached = ChaKind::hidden(path, &meta);
pub async fn from_follow(url: &Url, mut meta: Metadata) -> Self {
let mut attached = ChaKind::hidden(url, &meta);
if meta.is_symlink() {
attached |= ChaKind::LINK;
meta = fs::metadata(path).await.unwrap_or(meta);
meta = services::metadata(url).await.unwrap_or(meta);
}
if meta.is_symlink() {
attached |= ChaKind::ORPHAN;

View file

@ -1,6 +1,7 @@
use std::{fs::Metadata, path::Path};
use std::fs::Metadata;
use bitflags::bitflags;
use yazi_shared::url::Url;
bitflags! {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
@ -19,11 +20,11 @@ bitflags! {
impl ChaKind {
#[inline]
pub(super) fn hidden(_path: &Path, _meta: &Metadata) -> Self {
pub(super) fn hidden(_url: &Url, _meta: &Metadata) -> Self {
let mut me = Self::empty();
#[cfg(unix)]
if yazi_shared::url::Urn::new(_path).is_hidden() {
if _url.urn().is_hidden() {
me |= Self::HIDDEN;
}
#[cfg(windows)]

View file

@ -1,10 +1,9 @@
use std::{ffi::OsStr, fs::{FileType, Metadata}, hash::{BuildHasher, Hash, Hasher}, ops::Deref};
use anyhow::Result;
use tokio::fs;
use yazi_shared::url::{Url, Urn, UrnBuf};
use crate::cha::Cha;
use crate::{cha::Cha, services};
#[derive(Clone, Debug, Default)]
pub struct File {
@ -23,14 +22,13 @@ impl Deref for File {
impl File {
#[inline]
pub async fn new(url: Url) -> Result<Self> {
let meta = fs::symlink_metadata(&url).await?;
let meta = services::symlink_metadata(&url).await?;
Ok(Self::from_follow(url, meta).await)
}
#[inline]
pub async fn from_follow(url: Url, meta: Metadata) -> Self {
let link_to =
if meta.is_symlink() { fs::read_link(&url).await.map(Url::from).ok() } else { None };
let link_to = if meta.is_symlink() { services::read_link(&url).await.ok() } else { None };
let cha = Cha::from_follow(&url, meta).await;

View file

@ -1,10 +1,10 @@
use std::{collections::{HashMap, HashSet}, mem, ops::{Deref, Not}};
use tokio::{fs::{self, DirEntry}, select, sync::mpsc::{self, UnboundedReceiver}};
use tokio::{select, sync::mpsc::{self, UnboundedReceiver}};
use yazi_shared::{Id, url::{Url, Urn, UrnBuf}};
use super::{FilesSorter, Filter};
use crate::{FILES_TICKET, File, FilesOp, SortBy, cha::Cha, mounts::PARTITIONS};
use crate::{FILES_TICKET, File, FilesOp, SortBy, cha::Cha, mounts::PARTITIONS, services};
#[derive(Default)]
pub struct Files {
@ -31,7 +31,7 @@ impl Files {
pub fn new(show_hidden: bool) -> Self { Self { show_hidden, ..Default::default() } }
pub async fn from_dir(dir: &Url) -> std::io::Result<UnboundedReceiver<File>> {
let mut it = fs::read_dir(dir).await?;
let mut it = services::read_dir(dir).await?;
let (tx, rx) = mpsc::unbounded_channel();
tokio::spawn(async move {
@ -52,7 +52,7 @@ impl Files {
}
pub async fn from_dir_bulk(dir: &Url) -> std::io::Result<Vec<File>> {
let mut it = fs::read_dir(dir).await?;
let mut it = services::read_dir(dir).await?;
let mut entries = Vec::with_capacity(5000);
while let Ok(Some(entry)) = it.next_entry().await {
entries.push(entry);
@ -60,7 +60,7 @@ impl Files {
let (first, rest) = entries.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: &[tokio::fs::DirEntry]) -> Vec<File> {
let mut files = Vec::with_capacity(entries.len());
for entry in entries {
let url = Url::from(entry.path());

View file

@ -1,3 +1,5 @@
// FIXME: VFS
use std::{borrow::Cow, collections::{HashMap, HashSet}, ffi::{OsStr, OsString}, path::{Path, PathBuf}};
use anyhow::{Result, bail};

View file

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

View file

@ -1,9 +1,8 @@
use std::{borrow::Cow, env, ffi::{OsStr, OsString}, future::Future, io, path::{Component, Path, PathBuf}};
use tokio::fs;
use yazi_shared::url::{Loc, Url};
use yazi_shared::url::Url;
use crate::CWD;
use crate::{CWD, services};
#[inline]
pub fn clean_url(url: &Url) -> Url { Url::from(clean_path(url)) }
@ -83,19 +82,19 @@ pub async fn unique_name<F>(u: Url, append: F) -> io::Result<Url>
where
F: Future<Output = bool>,
{
match fs::symlink_metadata(&u).await {
match services::symlink_metadata(&u).await {
Ok(_) => _unique_name(u, append.await).await,
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(u),
Err(e) => Err(e),
}
}
async fn _unique_name(mut u: Url, append: bool) -> io::Result<Url> {
let Some(stem) = u.file_stem().map(|s| s.to_owned()) else {
async fn _unique_name(mut url: Url, append: bool) -> io::Result<Url> {
let Some(stem) = url.file_stem().map(|s| s.to_owned()) else {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty file stem"));
};
let dot_ext = u.extension().map_or_else(OsString::new, |e| {
let dot_ext = url.extension().map_or_else(OsString::new, |e| {
let mut s = OsString::with_capacity(e.len() + 1);
s.push(".");
s.push(e);
@ -103,9 +102,9 @@ async fn _unique_name(mut u: Url, append: bool) -> io::Result<Url> {
});
let mut i = 1u64;
let mut p = u.to_path();
let mut name = OsString::with_capacity(stem.len() + dot_ext.len() + 5);
loop {
let mut name = OsString::with_capacity(stem.len() + dot_ext.len() + 5);
name.clear();
name.push(&stem);
if append {
@ -118,16 +117,15 @@ async fn _unique_name(mut u: Url, append: bool) -> io::Result<Url> {
name.push(&dot_ext);
}
p.set_file_name(name);
match fs::symlink_metadata(&p).await {
url.set_name(&name);
match services::symlink_metadata(&url).await {
Ok(_) => i += 1,
Err(e) if e.kind() == io::ErrorKind::NotFound => break,
Err(e) => return Err(e),
}
}
u.set_loc(Loc::from(u.base(), p));
Ok(u)
Ok(url)
}
// Parameters

View file

@ -0,0 +1,83 @@
use std::{io, path::{Path, PathBuf}};
pub struct Local;
impl Local {
#[inline]
pub async fn canonicalize(path: impl AsRef<Path>) -> io::Result<PathBuf> {
tokio::fs::canonicalize(path).await
}
#[inline]
pub async fn create(path: impl AsRef<Path>) -> io::Result<tokio::fs::File> {
tokio::fs::File::create(path).await
}
#[inline]
pub async fn create_dir(path: impl AsRef<Path>) -> io::Result<()> {
tokio::fs::create_dir(path).await
}
#[inline]
pub async fn create_dir_all(path: impl AsRef<Path>) -> io::Result<()> {
tokio::fs::create_dir_all(path).await
}
#[inline]
pub async fn metadata(url: impl AsRef<Path>) -> io::Result<std::fs::Metadata> {
tokio::fs::metadata(url).await
}
#[inline]
pub async fn open(path: impl AsRef<Path>) -> io::Result<tokio::fs::File> {
tokio::fs::File::open(path).await
}
#[inline]
pub async fn read(path: impl AsRef<Path>) -> io::Result<Vec<u8>> { tokio::fs::read(path).await }
#[inline]
pub async fn read_dir(path: impl AsRef<Path>) -> io::Result<tokio::fs::ReadDir> {
tokio::fs::read_dir(path).await
}
#[inline]
pub async fn read_link(url: impl AsRef<Path>) -> io::Result<PathBuf> {
tokio::fs::read_link(url).await
}
#[inline]
pub async fn read_to_string(path: impl AsRef<Path>) -> io::Result<String> {
tokio::fs::read_to_string(path).await
}
#[inline]
pub async fn remove_dir(path: impl AsRef<Path>) -> io::Result<()> {
tokio::fs::remove_dir(path).await
}
#[inline]
pub async fn remove_dir_all(path: impl AsRef<Path>) -> io::Result<()> {
tokio::fs::remove_dir_all(path).await
}
#[inline]
pub async fn remove_file(path: impl AsRef<Path>) -> io::Result<()> {
tokio::fs::remove_file(path).await
}
#[inline]
pub async fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
tokio::fs::rename(from, to).await
}
#[inline]
pub async fn symlink_metadata(path: impl AsRef<Path>) -> io::Result<std::fs::Metadata> {
tokio::fs::symlink_metadata(path).await
}
#[inline]
pub async fn write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> io::Result<()> {
tokio::fs::write(path, contents).await
}
}

View file

@ -0,0 +1 @@
yazi_macro::mod_flat!(local services);

View file

@ -0,0 +1,75 @@
use std::io;
use yazi_shared::url::Url;
use crate::services::Local;
#[inline]
pub async fn canonicalize(url: impl AsRef<Url>) -> io::Result<Url> {
Local::canonicalize(url.as_ref()).await.map(Into::into)
}
#[inline]
pub async fn create(url: impl AsRef<Url>) -> io::Result<tokio::fs::File> {
Local::create(url.as_ref()).await
}
#[inline]
pub async fn create_dir(url: impl AsRef<Url>) -> io::Result<()> {
Local::create_dir(url.as_ref()).await
}
#[inline]
pub async fn create_dir_all(url: impl AsRef<Url>) -> io::Result<()> {
Local::create_dir_all(url.as_ref()).await
}
#[inline]
pub async fn metadata(url: impl AsRef<Url>) -> io::Result<std::fs::Metadata> {
Local::metadata(url.as_ref()).await
}
#[inline]
pub async fn open(url: impl AsRef<Url>) -> io::Result<tokio::fs::File> {
Local::open(url.as_ref()).await
}
#[inline]
pub async fn read_dir(url: impl AsRef<Url>) -> io::Result<tokio::fs::ReadDir> {
Local::read_dir(url.as_ref()).await
}
#[inline]
pub async fn read_link(url: impl AsRef<Url>) -> io::Result<Url> {
Local::read_link(url.as_ref()).await.map(Into::into)
}
#[inline]
pub async fn remove_dir(url: impl AsRef<Url>) -> io::Result<()> {
Local::remove_dir(url.as_ref()).await
}
#[inline]
pub async fn remove_dir_all(url: impl AsRef<Url>) -> io::Result<()> {
Local::remove_dir_all(url.as_ref()).await
}
#[inline]
pub async fn remove_file(url: impl AsRef<Url>) -> io::Result<()> {
Local::remove_file(url.as_ref()).await
}
#[inline]
pub async fn rename(from: impl AsRef<Url>, to: impl AsRef<Url>) -> io::Result<()> {
Local::rename(from.as_ref(), to.as_ref()).await
}
#[inline]
pub async fn symlink_metadata(url: impl AsRef<Url>) -> io::Result<std::fs::Metadata> {
Local::symlink_metadata(url.as_ref()).await
}
#[inline]
pub async fn write(url: impl AsRef<Url>, contents: impl AsRef<[u8]>) -> io::Result<()> {
Local::write(url.as_ref(), contents).await
}

View file

@ -3,8 +3,9 @@ use std::{borrow::Cow, io::Cursor, mem, path::{Path, PathBuf}, sync::OnceLock};
use anyhow::{Result, anyhow};
use ratatui::{layout::Size, text::{Line, Span, Text}};
use syntect::{LoadingError, dumps, easy::HighlightLines, highlighting::{self, Theme, ThemeSet}, parsing::{SyntaxReference, SyntaxSet}};
use tokio::{fs::File, io::{AsyncBufReadExt, BufReader}};
use tokio::io::{AsyncBufReadExt, BufReader};
use yazi_config::{THEME, YAZI, preview::PreviewWrap};
use yazi_fs::services::Local;
use yazi_shared::{Ids, errors::PeekError, replace_to_printable};
static INCR: Ids = Ids::new();
@ -38,7 +39,7 @@ impl Highlighter {
pub fn abort() { INCR.next(); }
pub async fn highlight(&self, skip: usize, size: Size) -> Result<Text<'static>, PeekError> {
let mut reader = BufReader::new(File::open(&self.path).await?);
let mut reader = BufReader::new(Local::open(&self.path).await?);
let syntax = Self::find_syntax(&self.path).await;
let mut plain = syntax.is_err();
@ -142,7 +143,7 @@ impl Highlighter {
}
let mut line = String::new();
let mut reader = BufReader::new(File::open(&path).await?);
let mut reader = BufReader::new(Local::open(&path).await?);
reader.read_line(&mut line).await?;
syntaxes.find_syntax_by_first_line(&line).ok_or_else(|| anyhow!("No syntax found"))
}

View file

@ -1,8 +1,7 @@
use globset::GlobBuilder;
use mlua::{ExternalError, ExternalResult, Function, IntoLua, IntoLuaMulti, Lua, Table, Value};
use tokio::fs;
use yazi_binding::{Cha, Composer, ComposerGet, ComposerSet, Error, File, Url, UrlRef};
use yazi_fs::{mounts::PARTITIONS, remove_dir_clean};
use yazi_fs::{mounts::PARTITIONS, remove_dir_clean, services};
use crate::bindings::SizeCalculator;
@ -49,9 +48,9 @@ fn cwd(lua: &Lua) -> mlua::Result<Function> {
fn cha(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (url, follow): (UrlRef, Option<bool>)| async move {
let meta = if follow.unwrap_or(false) {
fs::metadata(&*url).await
services::metadata(&*url).await
} else {
fs::symlink_metadata(&*url).await
services::symlink_metadata(&*url).await
};
match meta {
@ -63,7 +62,7 @@ fn cha(lua: &Lua) -> mlua::Result<Function> {
fn write(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (url, data): (UrlRef, mlua::String)| async move {
match fs::write(&*url, data.as_bytes()).await {
match services::write(&*url, data.as_bytes()).await {
Ok(()) => true.into_lua_multi(&lua),
Err(e) => (false, Error::Io(e)).into_lua_multi(&lua),
}
@ -73,8 +72,8 @@ fn write(lua: &Lua) -> mlua::Result<Function> {
fn create(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (r#type, url): (mlua::String, UrlRef)| async move {
let result = match r#type.as_bytes().as_ref() {
b"dir" => fs::create_dir(&*url).await,
b"dir_all" => fs::create_dir_all(&*url).await,
b"dir" => services::create_dir(&*url).await,
b"dir_all" => services::create_dir_all(&*url).await,
_ => Err("Creation type must be 'dir' or 'dir_all'".into_lua_err())?,
};
@ -88,9 +87,9 @@ fn create(lua: &Lua) -> mlua::Result<Function> {
fn remove(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (r#type, url): (mlua::String, UrlRef)| async move {
let result = match r#type.as_bytes().as_ref() {
b"file" => fs::remove_file(&*url).await,
b"dir" => fs::remove_dir(&*url).await,
b"dir_all" => fs::remove_dir_all(&*url).await,
b"file" => services::remove_file(&*url).await,
b"dir" => services::remove_dir(&*url).await,
b"dir_all" => services::remove_dir_all(&*url).await,
b"dir_clean" => Ok(remove_dir_clean(&url).await),
_ => Err("Removal type must be 'file', 'dir', 'dir_all', or 'dir_clean'".into_lua_err())?,
};
@ -122,7 +121,7 @@ fn read_dir(lua: &Lua) -> mlua::Result<Function> {
let limit = options.raw_get("limit").unwrap_or(usize::MAX);
let resolve = options.raw_get("resolve").unwrap_or(false);
let mut it = match fs::read_dir(&*dir).await {
let mut it = match services::read_dir(&*dir).await {
Ok(it) => it,
Err(e) => return (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
};

View file

@ -3,8 +3,8 @@ use std::{borrow::Cow, collections::HashMap, ops::Deref};
use anyhow::{Context, Result, bail};
use mlua::{ChunkMode, ExternalError, Lua, Table};
use parking_lot::RwLock;
use tokio::fs;
use yazi_boot::BOOT;
use yazi_fs::services::Local;
use yazi_macro::plugin_preset as preset;
use yazi_shared::{LOG_LEVEL, RoCell};
@ -61,7 +61,7 @@ impl Loader {
let p = BOOT.plugin_dir.join(format!("{name}.yazi/main.lua"));
let chunk =
fs::read(&p).await.with_context(|| format!("Failed to load plugin from {p:?}"))?.into();
Local::read(&p).await.with_context(|| format!("Failed to load plugin from {p:?}"))?.into();
let result = Self::compatible_or_error(name, &chunk);
let inspect = f(&chunk);

View file

@ -1,10 +1,11 @@
use std::{borrow::Cow, collections::VecDeque, path::Path};
// FIXME: VFS, depends on yazi_fs::fns
use std::{borrow::Cow, collections::VecDeque};
use anyhow::{Result, anyhow};
use tokio::{fs::{self, DirEntry}, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc};
use tracing::warn;
use yazi_config::YAZI;
use yazi_fs::{SizeCalculator, cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, path_relative_to, skip_path};
use yazi_fs::{SizeCalculator, cha::Cha, copy_with_progress, maybe_exists, ok_or_not_found, path_relative_to, services, skip_path};
use yazi_shared::{Id, url::Url};
use super::{FileIn, FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash};
@ -327,17 +328,17 @@ impl File {
}
#[inline]
async fn cha(path: &Path, follow: bool) -> io::Result<Cha> {
let meta = fs::symlink_metadata(path).await?;
Ok(if follow { Cha::from_follow(path, meta).await } else { Cha::new(path, meta) })
async fn cha(url: &Url, follow: bool) -> io::Result<Cha> {
let meta = services::symlink_metadata(url).await?;
Ok(if follow { Cha::from_follow(url, meta).await } else { Cha::new(url, meta) })
}
#[inline]
async fn cha_from(entry: DirEntry, path: &Path, follow: bool) -> io::Result<Cha> {
async fn cha_from(entry: DirEntry, url: &Url, follow: bool) -> io::Result<Cha> {
Ok(if follow {
Cha::from_follow(path, entry.metadata().await?).await
Cha::from_follow(url, entry.metadata().await?).await
} else {
Cha::new(path, entry.metadata().await?)
Cha::new(url, entry.metadata().await?)
})
}
}

View file

@ -3,10 +3,10 @@ use std::{ffi::OsString, future::Future, sync::Arc, time::Duration};
use anyhow::Result;
use futures::{FutureExt, future::BoxFuture};
use parking_lot::Mutex;
use tokio::{fs, select, sync::mpsc::{self, UnboundedReceiver}, task::JoinHandle};
use tokio::{select, sync::mpsc::{self, UnboundedReceiver}, task::JoinHandle};
use yazi_config::{YAZI, plugin::{Fetcher, Preloader}};
use yazi_dds::Pump;
use yazi_fs::{must_be_dir, remove_dir_clean, unique_name};
use yazi_fs::{must_be_dir, remove_dir_clean, services, unique_name};
use yazi_parser::{app::PluginOpt, tasks::ProcessExecOpt};
use yazi_proxy::MgrProxy;
use yazi_shared::{Id, Throttle, url::Url};
@ -166,7 +166,7 @@ impl Scheduler {
move |canceled: bool| {
async move {
if !canceled {
fs::remove_dir_all(&target).await.ok();
services::remove_dir_all(&target).await.ok();
MgrProxy::update_tasks(&target);
Pump::push_delete(target);
}

View file

@ -95,6 +95,22 @@ impl Loc {
}
}
pub fn set_name(&mut self, name: impl AsRef<OsStr>) {
let name = name.as_ref();
if name == self.name() {
return;
}
if self.name > name.len() {
self.urn -= self.name - name.len();
} else {
self.urn += name.len() - self.name;
}
self.name = name.len();
self.path.set_file_name(name);
}
#[inline]
pub fn base(&self) -> &Path {
Path::new(unsafe {
@ -122,6 +138,8 @@ impl Loc {
#[cfg(test)]
mod tests {
use std::path::MAIN_SEPARATOR;
use super::*;
#[test]
@ -159,4 +177,22 @@ mod tests {
assert_eq!(loc.name(), OsStr::new("foo"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
}
#[test]
fn test_set_name() {
let mut loc = Loc::from(Path::new("/root"), "/root/code/foo/".into());
assert_eq!(loc.urn().as_os_str(), OsStr::new("code/foo"));
assert_eq!(loc.name(), OsStr::new("foo"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
loc.set_name("bar.txt");
assert_eq!(loc.urn().as_os_str(), OsString::from(format!("code{MAIN_SEPARATOR}bar.txt")));
assert_eq!(loc.name(), OsStr::new("bar.txt"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
loc.set_name("baz");
assert_eq!(loc.urn().as_os_str(), OsString::from(format!("code{MAIN_SEPARATOR}baz")));
assert_eq!(loc.name(), OsStr::new("baz"));
assert_eq!(loc.base().as_os_str(), OsStr::new("/root/"));
}
}

View file

@ -167,6 +167,9 @@ impl Url {
})
}
#[inline]
pub fn set_name(&mut self, name: impl AsRef<OsStr>) { self.loc.set_name(name); }
#[inline]
pub fn pair(&self) -> Option<(Self, UrnBuf)> { Some((self.parent_url()?, self.loc.urn_owned())) }
@ -240,9 +243,7 @@ impl Url {
#[inline]
pub fn set_loc(&mut self, loc: Loc) { self.loc = loc; }
#[inline]
pub fn to_path(&self) -> PathBuf { self.loc.to_path_buf() }
// FIXME: remove
#[inline]
pub fn into_path(self) -> PathBuf { self.loc.into_path() }