feat: new fs.copy() and fs.rename() APIs (#3467)

This commit is contained in:
三咲雅 misaki masa 2025-12-27 17:52:15 +08:00 committed by GitHub
parent c5a3e42986
commit 77d36ff5a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 711 additions and 179 deletions

View file

@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Zoom in or out of the preview image ([#2864])
- Improve the UX of the pick and input components ([#2906], [#2935])
- Show progress of each task in task manager ([#3121], [#3131], [#3134])
- New `fs.copy()` and `fs.rename()` APIs ([#3467])
- New experimental `ya.async()` API ([#3422])
- New `overall` option to set the overall background color ([#3317])
- Rounded corners for indicator bar ([#3419])
@ -1560,3 +1561,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#3422]: https://github.com/sxyazi/yazi/pull/3422
[#3429]: https://github.com/sxyazi/yazi/pull/3429
[#3456]: https://github.com/sxyazi/yazi/pull/3456
[#3467]: https://github.com/sxyazi/yazi/pull/3467

745
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -26,7 +26,7 @@ lto = false
debug = false
[workspace.dependencies]
ansi-to-tui = "7.0.0"
ansi-to-tui = "8.0.0"
anyhow = "1.0.100"
base64 = "0.22.1"
bitflags = { version = "2.10.0", features = [ "serde" ] }
@ -48,12 +48,12 @@ parking_lot = "0.12.5"
paste = "1.0.15"
percent-encoding = "2.3.2"
rand = { version = "0.9.2", default-features = false, features = [ "os_rng", "small_rng", "std" ] }
ratatui = { version = "0.29.0", features = [ "serde", "unstable-rendered-line-info", "unstable-widget-ref" ] }
ratatui = { version = "0.30.0", features = [ "serde", "unstable-rendered-line-info", "unstable-widget-ref" ] }
regex = "1.12.2"
russh = { version = "0.56.0", default-features = false, features = [ "ring", "rsa" ] }
scopeguard = "1.2.0"
serde = { version = "1.0.228", features = [ "derive" ] }
serde_json = "1.0.146"
serde_json = "1.0.147"
syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
thiserror = "2.0.17"
tokio = { version = "1.48.0", features = [ "full" ] }
@ -63,7 +63,7 @@ toml = { version = "0.9.10" }
tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_debug" ] }
twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] }
typed-path = "0.12.0"
unicode-width = { version = "0.2.0", default-features = false }
unicode-width = { version = "0.2.2", default-features = false }
uzers = "0.12.2"
[workspace.lints.clippy]

View file

@ -20,7 +20,7 @@ pub struct Border {
pub r#type: ratatui::widgets::BorderType,
pub style: ratatui::style::Style,
pub titles: Vec<(ratatui::widgets::block::Position, ratatui::text::Line<'static>)>,
pub titles: Vec<(ratatui::widgets::TitlePosition, ratatui::text::Line<'static>)>,
}
impl Border {
@ -51,8 +51,8 @@ impl Border {
for title in self.titles {
block = match title {
(ratatui::widgets::block::Position::Top, line) => block.title(line),
(ratatui::widgets::block::Position::Bottom, line) => block.title(line),
(ratatui::widgets::TitlePosition::Top, line) => block.title(line),
(ratatui::widgets::TitlePosition::Bottom, line) => block.title(line),
};
}
@ -80,9 +80,9 @@ impl UserData for Border {
"title",
|_, (ud, line, position): (AnyUserData, Value, Option<u8>)| {
let position = if position == Some(Borders::BOTTOM.bits()) {
ratatui::widgets::block::Position::Bottom
ratatui::widgets::TitlePosition::Bottom
} else {
ratatui::widgets::block::Position::Top
ratatui::widgets::TitlePosition::Top
};
ud.borrow_mut::<Self>()?.titles.push((position, Line::try_from(line)?.inner));

View file

@ -1,4 +1,4 @@
use ratatui::{buffer::Buffer, layout::{self, Alignment, Constraint, Rect}, text::Line, widgets::{Block, BorderType, Widget, WidgetRef}};
use ratatui::{buffer::Buffer, layout::{self, Alignment, Constraint, Rect}, text::Line, widgets::{Block, BorderType, Widget}};
use yazi_config::THEME;
use yazi_core::{Core, tasks::TASKS_PERCENT};
@ -39,7 +39,7 @@ impl Widget for Tasks<'_> {
.title_alignment(Alignment::Center)
.border_type(BorderType::Rounded)
.border_style(THEME.tasks.border);
block.render_ref(area, buf);
(&block).render(area, buf);
List::new(self.core).render(block.inner(area), buf);
}

View file

@ -2,7 +2,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::cha::{Cha, ChaMode};
#[derive(Clone, Copy, Debug, Default)]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Attrs {
pub mode: Option<ChaMode>,
pub atime: Option<SystemTime>,
@ -16,35 +16,57 @@ impl From<Cha> for Attrs {
}
}
impl From<Attrs> for std::fs::FileTimes {
fn from(attrs: Attrs) -> Self {
let mut t = Self::new();
impl TryFrom<Attrs> for std::fs::FileTimes {
type Error = ();
if let Some(atime) = attrs.atime {
fn try_from(value: Attrs) -> Result<Self, Self::Error> {
if !value.has_times() {
return Err(());
}
let mut t = Self::new();
if let Some(atime) = value.atime {
t = t.set_accessed(atime);
}
#[cfg(target_os = "macos")]
if let Some(btime) = attrs.btime {
if let Some(btime) = value.btime {
use std::os::macos::fs::FileTimesExt;
t = t.set_created(btime);
}
#[cfg(windows)]
if let Some(btime) = attrs.btime {
if let Some(btime) = value.btime {
use std::os::windows::fs::FileTimesExt;
t = t.set_created(btime);
}
if let Some(mtime) = attrs.mtime {
if let Some(mtime) = value.mtime {
t = t.set_modified(mtime);
}
t
Ok(t)
}
}
impl TryFrom<Attrs> for std::fs::Permissions {
type Error = ();
fn try_from(value: Attrs) -> Result<Self, Self::Error> {
#[cfg(unix)]
if let Some(mode) = value.mode {
return Ok(mode.into());
}
Err(())
}
}
impl Attrs {
pub fn has_times(self) -> bool {
self.atime.is_some() || self.btime.is_some() || self.mtime.is_some()
}
pub fn atime_dur(self) -> Option<Duration> { self.atime?.duration_since(UNIX_EPOCH).ok() }
pub fn btime_dur(self) -> Option<Duration> { self.btime?.duration_since(UNIX_EPOCH).ok() }

View file

@ -22,7 +22,9 @@ pub(super) async fn copy_impl(from: PathBuf, to: PathBuf, attrs: Attrs) -> io::R
if let Some(mode) = attrs.mode {
writer.set_permissions(mode.into()).ok();
}
writer.set_times(attrs.into()).ok();
if let Ok(times) = attrs.try_into() {
writer.set_times(times).ok();
}
Ok(written)
})
@ -34,8 +36,10 @@ pub(super) async fn copy_impl(from: PathBuf, to: PathBuf, attrs: Attrs) -> io::R
tokio::task::spawn_blocking(move || {
let written = std::fs::copy(from, &to)?;
if let Ok(file) = std::fs::File::options().write(true).open(to) {
file.set_times(attrs.into()).ok();
if let Ok(times) = attrs.try_into()
&& let Ok(file) = std::fs::File::options().write(true).open(to)
{
file.set_times(times).ok();
}
Ok(written)

View file

@ -3,7 +3,7 @@ use std::str::FromStr;
use mlua::{ExternalError, Function, IntoLua, IntoLuaMulti, Lua, Table, Value};
use yazi_binding::{Cha, Composer, ComposerGet, ComposerSet, Error, File, Url, UrlRef};
use yazi_config::Pattern;
use yazi_fs::{mounts::PARTITIONS, provider::{DirReader, FileHolder}};
use yazi_fs::{mounts::PARTITIONS, provider::{Attrs, DirReader, FileHolder}};
use yazi_shared::url::{UrlCow, UrlLike};
use yazi_vfs::{VfsFile, provider};
@ -15,9 +15,11 @@ pub fn compose() -> Composer<ComposerGet, ComposerSet> {
b"op" => op(lua)?,
b"cwd" => cwd(lua)?,
b"cha" => cha(lua)?,
b"copy" => copy(lua)?,
b"write" => write(lua)?,
b"create" => create(lua)?,
b"remove" => remove(lua)?,
b"rename" => rename(lua)?,
b"read_dir" => read_dir(lua)?,
b"calc_size" => calc_size(lua)?,
b"expand_url" => expand_url(lua)?,
@ -64,6 +66,15 @@ fn cha(lua: &Lua) -> mlua::Result<Function> {
})
}
fn copy(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (from, to): (UrlRef, UrlRef)| async move {
match provider::copy(&*from, &*to, Attrs::default()).await {
Ok(len) => len.into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
}
})
}
fn write(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (url, data): (UrlRef, mlua::String)| async move {
match provider::write(&*url, data.as_bytes()).await {
@ -105,6 +116,15 @@ fn remove(lua: &Lua) -> mlua::Result<Function> {
})
}
fn rename(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (from, to): (UrlRef, UrlRef)| async move {
match provider::rename(&*from, &*to).await {
Ok(()) => true.into_lua_multi(&lua),
Err(e) => (false, Error::Io(e)).into_lua_multi(&lua),
}
})
}
fn read_dir(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (dir, options): (UrlRef, Table)| async move {
let pat = if let Ok(s) = options.raw_get::<mlua::String>("glob") {

View file

@ -23,7 +23,7 @@ impl Utils {
r#type: ratatui::widgets::BorderType::Rounded,
style: THEME.spot.border.into(),
titles: vec![(
ratatui::widgets::block::Position::Top,
ratatui::widgets::TitlePosition::Top,
ratatui::text::Line::raw("Spot").centered().style(THEME.spot.title),
)],
}),

View file

@ -2,7 +2,7 @@ use std::{collections::HashMap, fmt};
use serde::{Deserialize, Deserializer, Serialize, de::Visitor, ser::SerializeStruct};
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Attrs {
pub size: Option<u64>,
pub uid: Option<u32>,
@ -14,6 +14,8 @@ pub struct Attrs {
}
impl Attrs {
pub fn is_empty(&self) -> bool { *self == Self::default() }
pub fn len(&self) -> usize {
let mut len = 4;
if let Some(size) = self.size {

View file

@ -28,18 +28,22 @@ impl RwFile {
pub async fn set_attrs(&self, attrs: Attrs) -> io::Result<()> {
match self {
Self::Tokio(f) => {
let (perm, times) = (attrs.try_into(), attrs.try_into());
if perm.is_err() && times.is_err() {
return Ok(());
}
let std = f.try_clone().await?.into_std().await;
tokio::task::spawn_blocking(move || {
#[cfg(unix)]
if let Some(mode) = attrs.mode {
std.set_permissions(mode.into()).ok();
}
std.set_times(attrs.into()).ok();
perm.map(|p| std.set_permissions(p)).ok();
times.map(|t| std.set_times(t)).ok();
})
.await?;
}
Self::Sftp(f) => {
f.fsetstat(&super::sftp::Attrs(attrs).into()).await?;
if let Ok(attrs) = super::sftp::Attrs(attrs).try_into() {
f.fsetstat(&attrs).await?;
}
}
}

View file

@ -69,7 +69,7 @@ impl FileBuilder for Gate {
};
let flags = Flags::from(*self);
let attrs = super::Attrs(self.0.attrs).into();
let attrs = super::Attrs(self.0.attrs).try_into().unwrap_or_default();
Ok(Conn { name, config }.roll().await?.open(path, flags, &attrs).await?)
}

View file

@ -5,17 +5,21 @@ use yazi_fs::cha::ChaKind;
// --- Attrs
pub(crate) struct Attrs(pub(crate) yazi_fs::provider::Attrs);
impl From<Attrs> for yazi_sftp::fs::Attrs {
fn from(attrs: Attrs) -> Self {
Self {
impl TryFrom<Attrs> for yazi_sftp::fs::Attrs {
type Error = ();
fn try_from(value: Attrs) -> Result<Self, Self::Error> {
let attrs = Self {
size: None,
uid: None,
gid: None,
perm: attrs.0.mode.map(|m| m.bits() as u32),
atime: attrs.0.atime_dur().map(|d| d.as_secs() as u32),
mtime: attrs.0.mtime_dur().map(|d| d.as_secs() as u32),
perm: value.0.mode.map(|m| m.bits() as u32),
atime: value.0.atime_dur().map(|d| d.as_secs() as u32),
mtime: value.0.mtime_dur().map(|d| d.as_secs() as u32),
extended: Default::default(),
}
};
if attrs.is_empty() { Err(()) } else { Ok(attrs) }
}
}

View file

@ -78,7 +78,7 @@ impl<'a> Provider for Sftp<'a> {
P: AsPath,
{
let to = to.as_path().as_unix()?;
let attrs = Attrs::from(super::Attrs(attrs));
let attrs = super::Attrs(attrs).try_into().unwrap_or_default();
let op = self.op().await?;
let from = op.open(self.path, Flags::READ, &Attrs::default()).await?;
@ -89,7 +89,10 @@ impl<'a> Provider for Sftp<'a> {
let written = tokio::io::copy(&mut reader, &mut writer).await?;
writer.flush().await?;
writer.get_ref().fsetstat(&attrs).await.ok();
if !attrs.is_empty() {
writer.get_ref().fsetstat(&attrs).await.ok();
}
writer.shutdown().await.ok();
Ok(written)
}