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]) - Zoom in or out of the preview image ([#2864])
- Improve the UX of the pick and input components ([#2906], [#2935]) - Improve the UX of the pick and input components ([#2906], [#2935])
- Show progress of each task in task manager ([#3121], [#3131], [#3134]) - 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 experimental `ya.async()` API ([#3422])
- New `overall` option to set the overall background color ([#3317]) - New `overall` option to set the overall background color ([#3317])
- Rounded corners for indicator bar ([#3419]) - 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 [#3422]: https://github.com/sxyazi/yazi/pull/3422
[#3429]: https://github.com/sxyazi/yazi/pull/3429 [#3429]: https://github.com/sxyazi/yazi/pull/3429
[#3456]: https://github.com/sxyazi/yazi/pull/3456 [#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 debug = false
[workspace.dependencies] [workspace.dependencies]
ansi-to-tui = "7.0.0" ansi-to-tui = "8.0.0"
anyhow = "1.0.100" anyhow = "1.0.100"
base64 = "0.22.1" base64 = "0.22.1"
bitflags = { version = "2.10.0", features = [ "serde" ] } bitflags = { version = "2.10.0", features = [ "serde" ] }
@ -48,12 +48,12 @@ parking_lot = "0.12.5"
paste = "1.0.15" paste = "1.0.15"
percent-encoding = "2.3.2" percent-encoding = "2.3.2"
rand = { version = "0.9.2", default-features = false, features = [ "os_rng", "small_rng", "std" ] } 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" regex = "1.12.2"
russh = { version = "0.56.0", default-features = false, features = [ "ring", "rsa" ] } russh = { version = "0.56.0", default-features = false, features = [ "ring", "rsa" ] }
scopeguard = "1.2.0" scopeguard = "1.2.0"
serde = { version = "1.0.228", features = [ "derive" ] } 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" ] } syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
thiserror = "2.0.17" thiserror = "2.0.17"
tokio = { version = "1.48.0", features = [ "full" ] } 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" ] } 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" ] } twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] }
typed-path = "0.12.0" 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" uzers = "0.12.2"
[workspace.lints.clippy] [workspace.lints.clippy]

View file

@ -20,7 +20,7 @@ pub struct Border {
pub r#type: ratatui::widgets::BorderType, pub r#type: ratatui::widgets::BorderType,
pub style: ratatui::style::Style, 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 { impl Border {
@ -51,8 +51,8 @@ impl Border {
for title in self.titles { for title in self.titles {
block = match title { block = match title {
(ratatui::widgets::block::Position::Top, line) => block.title(line), (ratatui::widgets::TitlePosition::Top, line) => block.title(line),
(ratatui::widgets::block::Position::Bottom, line) => block.title(line), (ratatui::widgets::TitlePosition::Bottom, line) => block.title(line),
}; };
} }
@ -80,9 +80,9 @@ impl UserData for Border {
"title", "title",
|_, (ud, line, position): (AnyUserData, Value, Option<u8>)| { |_, (ud, line, position): (AnyUserData, Value, Option<u8>)| {
let position = if position == Some(Borders::BOTTOM.bits()) { let position = if position == Some(Borders::BOTTOM.bits()) {
ratatui::widgets::block::Position::Bottom ratatui::widgets::TitlePosition::Bottom
} else { } else {
ratatui::widgets::block::Position::Top ratatui::widgets::TitlePosition::Top
}; };
ud.borrow_mut::<Self>()?.titles.push((position, Line::try_from(line)?.inner)); 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_config::THEME;
use yazi_core::{Core, tasks::TASKS_PERCENT}; use yazi_core::{Core, tasks::TASKS_PERCENT};
@ -39,7 +39,7 @@ impl Widget for Tasks<'_> {
.title_alignment(Alignment::Center) .title_alignment(Alignment::Center)
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(THEME.tasks.border); .border_style(THEME.tasks.border);
block.render_ref(area, buf); (&block).render(area, buf);
List::new(self.core).render(block.inner(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}; use crate::cha::{Cha, ChaMode};
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Attrs { pub struct Attrs {
pub mode: Option<ChaMode>, pub mode: Option<ChaMode>,
pub atime: Option<SystemTime>, pub atime: Option<SystemTime>,
@ -16,35 +16,57 @@ impl From<Cha> for Attrs {
} }
} }
impl From<Attrs> for std::fs::FileTimes { impl TryFrom<Attrs> for std::fs::FileTimes {
fn from(attrs: Attrs) -> Self { type Error = ();
let mut t = Self::new();
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); t = t.set_accessed(atime);
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
if let Some(btime) = attrs.btime { if let Some(btime) = value.btime {
use std::os::macos::fs::FileTimesExt; use std::os::macos::fs::FileTimesExt;
t = t.set_created(btime); t = t.set_created(btime);
} }
#[cfg(windows)] #[cfg(windows)]
if let Some(btime) = attrs.btime { if let Some(btime) = value.btime {
use std::os::windows::fs::FileTimesExt; use std::os::windows::fs::FileTimesExt;
t = t.set_created(btime); t = t.set_created(btime);
} }
if let Some(mtime) = attrs.mtime { if let Some(mtime) = value.mtime {
t = t.set_modified(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 { 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 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() } 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 { if let Some(mode) = attrs.mode {
writer.set_permissions(mode.into()).ok(); 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) 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 || { tokio::task::spawn_blocking(move || {
let written = std::fs::copy(from, &to)?; let written = std::fs::copy(from, &to)?;
if let Ok(file) = std::fs::File::options().write(true).open(to) { if let Ok(times) = attrs.try_into()
file.set_times(attrs.into()).ok(); && let Ok(file) = std::fs::File::options().write(true).open(to)
{
file.set_times(times).ok();
} }
Ok(written) Ok(written)

View file

@ -3,7 +3,7 @@ use std::str::FromStr;
use mlua::{ExternalError, Function, IntoLua, IntoLuaMulti, Lua, Table, Value}; use mlua::{ExternalError, Function, IntoLua, IntoLuaMulti, Lua, Table, Value};
use yazi_binding::{Cha, Composer, ComposerGet, ComposerSet, Error, File, Url, UrlRef}; use yazi_binding::{Cha, Composer, ComposerGet, ComposerSet, Error, File, Url, UrlRef};
use yazi_config::Pattern; 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_shared::url::{UrlCow, UrlLike};
use yazi_vfs::{VfsFile, provider}; use yazi_vfs::{VfsFile, provider};
@ -15,9 +15,11 @@ pub fn compose() -> Composer<ComposerGet, ComposerSet> {
b"op" => op(lua)?, b"op" => op(lua)?,
b"cwd" => cwd(lua)?, b"cwd" => cwd(lua)?,
b"cha" => cha(lua)?, b"cha" => cha(lua)?,
b"copy" => copy(lua)?,
b"write" => write(lua)?, b"write" => write(lua)?,
b"create" => create(lua)?, b"create" => create(lua)?,
b"remove" => remove(lua)?, b"remove" => remove(lua)?,
b"rename" => rename(lua)?,
b"read_dir" => read_dir(lua)?, b"read_dir" => read_dir(lua)?,
b"calc_size" => calc_size(lua)?, b"calc_size" => calc_size(lua)?,
b"expand_url" => expand_url(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> { fn write(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (url, data): (UrlRef, mlua::String)| async move { lua.create_async_function(|lua, (url, data): (UrlRef, mlua::String)| async move {
match provider::write(&*url, data.as_bytes()).await { 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> { fn read_dir(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, (dir, options): (UrlRef, Table)| async move { lua.create_async_function(|lua, (dir, options): (UrlRef, Table)| async move {
let pat = if let Ok(s) = options.raw_get::<mlua::String>("glob") { 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, r#type: ratatui::widgets::BorderType::Rounded,
style: THEME.spot.border.into(), style: THEME.spot.border.into(),
titles: vec![( titles: vec![(
ratatui::widgets::block::Position::Top, ratatui::widgets::TitlePosition::Top,
ratatui::text::Line::raw("Spot").centered().style(THEME.spot.title), 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}; use serde::{Deserialize, Deserializer, Serialize, de::Visitor, ser::SerializeStruct};
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Attrs { pub struct Attrs {
pub size: Option<u64>, pub size: Option<u64>,
pub uid: Option<u32>, pub uid: Option<u32>,
@ -14,6 +14,8 @@ pub struct Attrs {
} }
impl Attrs { impl Attrs {
pub fn is_empty(&self) -> bool { *self == Self::default() }
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
let mut len = 4; let mut len = 4;
if let Some(size) = self.size { if let Some(size) = self.size {

View file

@ -28,18 +28,22 @@ impl RwFile {
pub async fn set_attrs(&self, attrs: Attrs) -> io::Result<()> { pub async fn set_attrs(&self, attrs: Attrs) -> io::Result<()> {
match self { match self {
Self::Tokio(f) => { 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; let std = f.try_clone().await?.into_std().await;
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
#[cfg(unix)] perm.map(|p| std.set_permissions(p)).ok();
if let Some(mode) = attrs.mode { times.map(|t| std.set_times(t)).ok();
std.set_permissions(mode.into()).ok();
}
std.set_times(attrs.into()).ok();
}) })
.await?; .await?;
} }
Self::Sftp(f) => { 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 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?) Ok(Conn { name, config }.roll().await?.open(path, flags, &attrs).await?)
} }

View file

@ -5,17 +5,21 @@ use yazi_fs::cha::ChaKind;
// --- Attrs // --- Attrs
pub(crate) struct Attrs(pub(crate) yazi_fs::provider::Attrs); pub(crate) struct Attrs(pub(crate) yazi_fs::provider::Attrs);
impl From<Attrs> for yazi_sftp::fs::Attrs { impl TryFrom<Attrs> for yazi_sftp::fs::Attrs {
fn from(attrs: Attrs) -> Self { type Error = ();
Self {
fn try_from(value: Attrs) -> Result<Self, Self::Error> {
let attrs = Self {
size: None, size: None,
uid: None, uid: None,
gid: None, gid: None,
perm: attrs.0.mode.map(|m| m.bits() as u32), perm: value.0.mode.map(|m| m.bits() as u32),
atime: attrs.0.atime_dur().map(|d| d.as_secs() as u32), atime: value.0.atime_dur().map(|d| d.as_secs() as u32),
mtime: attrs.0.mtime_dur().map(|d| d.as_secs() as u32), mtime: value.0.mtime_dur().map(|d| d.as_secs() as u32),
extended: Default::default(), 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, P: AsPath,
{ {
let to = to.as_path().as_unix()?; 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 op = self.op().await?;
let from = op.open(self.path, Flags::READ, &Attrs::default()).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?; let written = tokio::io::copy(&mut reader, &mut writer).await?;
writer.flush().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(); writer.shutdown().await.ok();
Ok(written) Ok(written)
} }