From c68e2df8c0e2cf8a4511d33d8c3b1713994622dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Mon, 13 Oct 2025 19:50:46 +0800 Subject: [PATCH] feat: encode SFTP paths on Windows in a revised WTF-8 (#3238) --- Cargo.lock | 24 +- Cargo.toml | 3 +- yazi-actor/src/mgr/update_files.rs | 2 +- yazi-actor/src/mgr/update_mimes.rs | 2 +- yazi-actor/src/tasks/update_succeed.rs | 2 +- yazi-binding/src/macros.rs | 5 +- yazi-boot/src/actions/clear_cache.rs | 2 +- yazi-config/src/preview/preview.rs | 2 +- yazi-core/src/tasks/mod.rs | 2 +- .../src/tasks/{preload.rs => prework.rs} | 2 +- yazi-fm/Cargo.toml | 1 + yazi-fs/Cargo.toml | 29 +-- yazi-fs/src/cha/cha.rs | 4 +- yazi-fs/src/cwd.rs | 55 +++-- yazi-fs/src/file.rs | 5 +- yazi-fs/src/hash.rs | 47 ++++ yazi-fs/src/lib.rs | 2 +- yazi-fs/src/path/mod.rs | 2 +- yazi-fs/src/path/percent.rs | 35 +++ yazi-fs/src/url.rs | 56 +++-- yazi-fs/src/xdg.rs | 27 ++- yazi-plugin/Cargo.toml | 1 + yazi-plugin/src/lib.rs | 2 +- yazi-plugin/src/utils/cache.rs | 2 +- yazi-scheduler/Cargo.toml | 1 + yazi-scheduler/src/file/file.rs | 212 ++++++++++++++++-- yazi-scheduler/src/file/in.rs | 31 +++ yazi-scheduler/src/file/out.rs | 150 +++++++++++++ yazi-scheduler/src/file/progress.rs | 96 ++++++++ yazi-scheduler/src/in.rs | 10 +- yazi-scheduler/src/out.rs | 12 +- yazi-scheduler/src/prework/prework.rs | 2 +- yazi-scheduler/src/process/shell.rs | 17 +- yazi-scheduler/src/progress.rs | 20 +- yazi-scheduler/src/scheduler.rs | 14 +- yazi-sftp/Cargo.toml | 3 - yazi-sftp/src/byte_str.rs | 65 +++--- yazi-sftp/src/lib.rs | 2 + yazi-sftp/src/operator.rs | 65 +++--- yazi-sftp/src/requests/extended.rs | 10 +- yazi-sftp/src/requests/lstat.rs | 9 +- yazi-sftp/src/requests/mkdir.rs | 8 +- yazi-sftp/src/requests/open.rs | 8 +- yazi-sftp/src/requests/open_dir.rs | 9 +- yazi-sftp/src/requests/readlink.rs | 9 +- yazi-sftp/src/requests/realpath.rs | 9 +- yazi-sftp/src/requests/remove.rs | 9 +- yazi-sftp/src/requests/rename.rs | 10 +- yazi-sftp/src/requests/rmdir.rs | 9 +- yazi-sftp/src/requests/set_stat.rs | 9 +- yazi-sftp/src/requests/stat.rs | 9 +- yazi-sftp/src/requests/symlink.rs | 10 +- yazi-sftp/src/wtf.rs | 87 +++++++ yazi-shared/Cargo.toml | 2 +- yazi-shared/src/loc/loc.rs | 4 + yazi-shared/src/scheme/scheme.rs | 4 + yazi-shared/src/string.rs | 1 - yazi-shared/src/url/buf.rs | 5 +- yazi-shared/src/url/component.rs | 2 +- yazi-shared/src/url/cow.rs | 10 +- yazi-shared/src/url/display.rs | 2 +- yazi-shared/src/url/encode.rs | 12 +- yazi-shared/src/url/url.rs | 11 +- yazi-shim/Cargo.toml | 15 ++ yazi-shim/src/lib.rs | 1 + {yazi-plugin => yazi-shim}/src/twox.rs | 2 +- yazi-vfs/src/fns.rs | 22 +- yazi-watcher/Cargo.toml | 15 +- yazi-watcher/src/backend.rs | 57 +++++ yazi-watcher/src/backend/backend.rs | 74 ------ yazi-watcher/src/backend/local.rs | 41 ---- yazi-watcher/src/backend/mod.rs | 1 - yazi-watcher/src/lib.rs | 8 +- yazi-watcher/src/{ => local}/linked.rs | 23 +- yazi-watcher/src/local/local.rs | 79 +++++++ yazi-watcher/src/local/mod.rs | 5 + yazi-watcher/src/remote/mod.rs | 1 + yazi-watcher/src/remote/remote.rs | 40 ++++ yazi-watcher/src/reporter.rs | 57 +++++ yazi-watcher/src/watched.rs | 28 ++- yazi-watcher/src/watcher.rs | 111 ++++----- 81 files changed, 1393 insertions(+), 459 deletions(-) rename yazi-core/src/tasks/{preload.rs => prework.rs} (97%) create mode 100644 yazi-fs/src/hash.rs create mode 100644 yazi-fs/src/path/percent.rs create mode 100644 yazi-sftp/src/wtf.rs create mode 100644 yazi-shim/Cargo.toml create mode 100644 yazi-shim/src/lib.rs rename {yazi-plugin => yazi-shim}/src/twox.rs (87%) create mode 100644 yazi-watcher/src/backend.rs delete mode 100644 yazi-watcher/src/backend/backend.rs delete mode 100644 yazi-watcher/src/backend/local.rs delete mode 100644 yazi-watcher/src/backend/mod.rs rename yazi-watcher/src/{ => local}/linked.rs (68%) create mode 100644 yazi-watcher/src/local/local.rs create mode 100644 yazi-watcher/src/local/mod.rs create mode 100644 yazi-watcher/src/remote/mod.rs create mode 100644 yazi-watcher/src/remote/remote.rs create mode 100644 yazi-watcher/src/reporter.rs diff --git a/Cargo.lock b/Cargo.lock index 3b0401b6..3de89a67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2349,12 +2349,6 @@ dependencies = [ "serde", ] -[[package]] -name = "os_str_bytes" -version = "7.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63eceb7b5d757011a87d08eb2123db15d87fb0c281f65d101ce30a1e96c3ad5c" - [[package]] name = "p256" version = "0.13.2" @@ -4798,6 +4792,7 @@ name = "yazi-fm" version = "25.9.15" dependencies = [ "anyhow", + "base64", "better-panic", "crossterm 0.29.0", "fdlimit", @@ -4839,6 +4834,7 @@ version = "25.9.15" dependencies = [ "anyhow", "arc-swap", + "base64", "bitflags 2.9.4", "core-foundation-sys", "dirs", @@ -4847,6 +4843,7 @@ dependencies = [ "libc", "objc", "parking_lot", + "percent-encoding", "regex", "scopeguard", "serde", @@ -4859,6 +4856,7 @@ dependencies = [ "yazi-ffi", "yazi-macro", "yazi-shared", + "yazi-shim", ] [[package]] @@ -4923,6 +4921,7 @@ dependencies = [ "yazi-prebuilt", "yazi-proxy", "yazi-shared", + "yazi-shim", "yazi-vfs", "yazi-widgets", ] @@ -4951,6 +4950,7 @@ version = "25.9.15" dependencies = [ "anyhow", "async-priority-channel", + "foldhash 0.2.0", "futures", "hashbrown 0.16.0", "libc", @@ -4979,7 +4979,6 @@ name = "yazi-sftp" version = "0.1.0" dependencies = [ "bitflags 2.9.4", - "os_str_bytes", "parking_lot", "russh", "serde", @@ -5007,6 +5006,14 @@ dependencies = [ "yazi-macro", ] +[[package]] +name = "yazi-shim" +version = "25.9.15" +dependencies = [ + "twox-hash", + "yazi-macro", +] + [[package]] name = "yazi-term" version = "25.9.15" @@ -5045,12 +5052,15 @@ name = "yazi-watcher" version = "25.9.15" dependencies = [ "anyhow", + "base64", "hashbrown 0.16.0", "notify", "parking_lot", + "percent-encoding", "tokio", "tokio-stream", "tracing", + "twox-hash", "yazi-adapter", "yazi-dds", "yazi-fs", diff --git a/Cargo.toml b/Cargo.toml index c322f49e..53570d40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ objc = "0.2.7" ordered-float = { version = "5.1.0", features = [ "serde" ] } parking_lot = "0.12.5" paste = "1.0.15" +percent-encoding = "2.3.2" ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info", "unstable-widget-ref" ] } regex = "1.11.3" russh = { version = "0.54.4", default-features = false, features = [ "ring", "rsa" ] } @@ -52,6 +53,6 @@ tokio-stream = "0.1.17" tokio-util = "0.7.16" toml = { version = "0.9.7" } tracing = { version = "0.1.41", features = [ "max_level_debug", "release_max_level_debug" ] } -unicode-width = { version = "0.2.0", default-features = false } twox-hash = { version = "2.1.2", default-features = false, features = [ "std", "random", "xxhash3_128" ] } +unicode-width = { version = "0.2.0", default-features = false } uzers = "0.12.1" diff --git a/yazi-actor/src/mgr/update_files.rs b/yazi-actor/src/mgr/update_files.rs index 61392b71..4fe19653 100644 --- a/yazi-actor/src/mgr/update_files.rs +++ b/yazi-actor/src/mgr/update_files.rs @@ -4,7 +4,7 @@ use yazi_fs::FilesOp; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::UpdateFilesOpt; use yazi_shared::{data::Data, url::UrlLike}; -use yazi_watcher::LINKED; +use yazi_watcher::local::LINKED; use crate::{Actor, Ctx}; diff --git a/yazi-actor/src/mgr/update_mimes.rs b/yazi-actor/src/mgr/update_mimes.rs index aa423b1a..ecb89060 100644 --- a/yazi-actor/src/mgr/update_mimes.rs +++ b/yazi-actor/src/mgr/update_mimes.rs @@ -3,7 +3,7 @@ use hashbrown::HashMap; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::UpdateMimesOpt; use yazi_shared::{data::Data, pool::InternStr, url::{AsUrl, UrlCov}}; -use yazi_watcher::LINKED; +use yazi_watcher::local::LINKED; use crate::{Actor, Ctx}; diff --git a/yazi-actor/src/tasks/update_succeed.rs b/yazi-actor/src/tasks/update_succeed.rs index cf3b78c5..24bf2a59 100644 --- a/yazi-actor/src/tasks/update_succeed.rs +++ b/yazi-actor/src/tasks/update_succeed.rs @@ -13,7 +13,7 @@ impl Actor for UpdateSucceed { const NAME: &str = "update_succeed"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { - cx.mgr.watcher.push_files(opt.urls); + cx.mgr.watcher.report(opt.urls); succ!(); } } diff --git a/yazi-binding/src/macros.rs b/yazi-binding/src/macros.rs index 3ad48c5d..6bc50e69 100644 --- a/yazi-binding/src/macros.rs +++ b/yazi-binding/src/macros.rs @@ -166,7 +166,10 @@ macro_rules! impl_file_fields { #[macro_export] macro_rules! impl_file_methods { ($methods:ident) => { - $methods.add_method("hash", |_, me, ()| Ok(me.hash_u64())); + $methods.add_method("hash", |_, me, ()| { + use yazi_fs::FsHash64; + Ok(me.hash_u64()) + }); $methods.add_method("icon", |_, me, ()| { use $crate::Icon; diff --git a/yazi-boot/src/actions/clear_cache.rs b/yazi-boot/src/actions/clear_cache.rs index 90b03b46..2902fbf3 100644 --- a/yazi-boot/src/actions/clear_cache.rs +++ b/yazi-boot/src/actions/clear_cache.rs @@ -5,7 +5,7 @@ use super::Actions; impl Actions { pub(super) fn clear_cache() { - if YAZI.preview.cache_dir == Xdg::cache_dir() { + if YAZI.preview.cache_dir == *Xdg::cache_dir() { println!("Clearing cache directory: \n{:?}", YAZI.preview.cache_dir); std::fs::remove_dir_all(&YAZI.preview.cache_dir).unwrap(); } else { diff --git a/yazi-config/src/preview/preview.rs b/yazi-config/src/preview/preview.rs index ece50d42..c8ab411a 100644 --- a/yazi-config/src/preview/preview.rs +++ b/yazi-config/src/preview/preview.rs @@ -49,7 +49,7 @@ impl Preview { } self.cache_dir = if self.cache_dir.as_os_str().is_empty() { - Xdg::cache_dir() + Xdg::cache_dir().to_owned() } else if let Some(p) = expand_url(Url::regular(&self.cache_dir)).into_path() { p } else { diff --git a/yazi-core/src/tasks/mod.rs b/yazi-core/src/tasks/mod.rs index 5b056421..824981f0 100644 --- a/yazi-core/src/tasks/mod.rs +++ b/yazi-core/src/tasks/mod.rs @@ -1,4 +1,4 @@ -yazi_macro::mod_flat!(file plugin preload process tasks); +yazi_macro::mod_flat!(file plugin prework process tasks); pub const TASKS_BORDER: u16 = 2; pub const TASKS_PADDING: u16 = 2; diff --git a/yazi-core/src/tasks/preload.rs b/yazi-core/src/tasks/prework.rs similarity index 97% rename from yazi-core/src/tasks/preload.rs rename to yazi-core/src/tasks/prework.rs index 3139f14e..d44c6136 100644 --- a/yazi-core/src/tasks/preload.rs +++ b/yazi-core/src/tasks/prework.rs @@ -1,5 +1,5 @@ use yazi_config::{YAZI, plugin::MAX_PREWORKERS}; -use yazi_fs::{File, Files, SortBy}; +use yazi_fs::{File, Files, FsHash64, SortBy}; use super::Tasks; use crate::mgr::Mimetype; diff --git a/yazi-fm/Cargo.toml b/yazi-fm/Cargo.toml index 1f20ca23..58d6ae98 100644 --- a/yazi-fm/Cargo.toml +++ b/yazi-fm/Cargo.toml @@ -42,6 +42,7 @@ yazi-watcher = { path = "../yazi-watcher", version = "25.9.15" } yazi-widgets = { path = "../yazi-widgets", version = "25.9.15" } # External dependencies +base64 = { workspace = true } anyhow = { workspace = true } better-panic = "0.3.0" crossterm = { workspace = true } diff --git a/yazi-fs/Cargo.toml b/yazi-fs/Cargo.toml index 011a316f..41925c1b 100644 --- a/yazi-fs/Cargo.toml +++ b/yazi-fs/Cargo.toml @@ -12,21 +12,24 @@ repository = "https://github.com/sxyazi/yazi" yazi-ffi = { path = "../yazi-ffi", version = "25.9.15" } yazi-macro = { path = "../yazi-macro", version = "25.9.15" } yazi-shared = { path = "../yazi-shared", version = "25.9.15" } +yazi-shim = { path = "../yazi-shim", version = "25.9.15" } # External dependencies -anyhow = { workspace = true } -arc-swap = "1.7.1" -bitflags = { workspace = true } -dirs = { workspace = true } -foldhash = { workspace = true } -hashbrown = { workspace = true } -parking_lot = { workspace = true } -regex = { workspace = true } -scopeguard = { workspace = true } -serde = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } -twox-hash = { workspace = true } +anyhow = { workspace = true } +arc-swap = "1.7.1" +base64 = { workspace = true } +bitflags = { workspace = true } +dirs = { workspace = true } +foldhash = { workspace = true } +hashbrown = { workspace = true } +parking_lot = { workspace = true } +percent-encoding = { workspace = true } +regex = { workspace = true } +scopeguard = { workspace = true } +serde = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +twox-hash = { workspace = true } [target."cfg(unix)".dependencies] libc = { workspace = true } diff --git a/yazi-fs/src/cha/cha.rs b/yazi-fs/src/cha/cha.rs index 0fa50290..5159e453 100644 --- a/yazi-fs/src/cha/cha.rs +++ b/yazi-fs/src/cha/cha.rs @@ -112,10 +112,10 @@ impl Cha { pub fn hits(self, c: Self) -> bool { self.len == c.len && self.mtime == c.mtime - && unix_either!(self.ctime == c.ctime, true) + && self.ctime == c.ctime && self.btime == c.btime && self.kind == c.kind - && unix_either!(self.mode == c.mode, true) + && self.mode == c.mode } #[inline] diff --git a/yazi-fs/src/cwd.rs b/yazi-fs/src/cwd.rs index 6fa64dc8..70f184b6 100644 --- a/yazi-fs/src/cwd.rs +++ b/yazi-fs/src/cwd.rs @@ -1,9 +1,9 @@ -use std::{env::{current_dir, set_current_dir}, ops::Deref, path::PathBuf, sync::{Arc, atomic::{AtomicBool, Ordering}}}; +use std::{borrow::Cow, env::{current_dir, set_current_dir}, ops::Deref, path::{Path, PathBuf}, sync::{Arc, atomic::{AtomicBool, Ordering}}}; use arc_swap::ArcSwap; -use yazi_shared::{RoCell, url::{UrlBuf, UrlLike}}; +use yazi_shared::{RoCell, url::{AsUrl, Url, UrlBuf, UrlLike}}; -use crate::FsUrl; +use crate::{FsUrl, Xdg}; pub static CWD: RoCell = RoCell::new(); @@ -46,6 +46,35 @@ impl Cwd { true } + pub fn ensure(url: Url) -> Cow { + use std::io::ErrorKind::{AlreadyExists, NotADirectory, NotFound}; + + let Some(cache) = url.cache() else { + return url.loc.as_path().into(); + }; + + if !matches!(std::fs::create_dir_all(&cache), Err(e) if e.kind() == NotADirectory || e.kind() == AlreadyExists) + { + return cache.into(); + } + + let count = cache.strip_prefix(Xdg::cache_dir()).expect("under cache dir").components().count(); + for n in (0..count).rev() { + let mut it = cache.components(); + for _ in 0..n { + it.next_back().unwrap(); + } + match std::fs::remove_file(it.as_path()) { + Ok(_) => break, + Err(e) if e.kind() == NotFound => break, + Err(_) => {} + } + } + + std::fs::create_dir_all(&cache).ok(); + cache.into() + } + fn sync_cwd() { static SYNCING: AtomicBool = AtomicBool::new(false); if SYNCING.swap(true, Ordering::Relaxed) { @@ -53,29 +82,21 @@ impl Cwd { } tokio::task::spawn_blocking(move || { - let path = CWD.ensure_cache(); + let cwd = CWD.load(); + let path = Self::ensure(cwd.as_url()); _ = set_current_dir(&path); let cur = current_dir().unwrap_or_default(); - unsafe { std::env::set_var("PWD", path) } + unsafe { std::env::set_var("PWD", path.as_ref()) } SYNCING.store(false, Ordering::Relaxed); - let path = CWD.ensure_cache(); + let cwd = CWD.load(); + let path = Self::ensure(cwd.as_url()); if cur != path { set_current_dir(&path).ok(); - unsafe { std::env::set_var("PWD", path) } + unsafe { std::env::set_var("PWD", path.as_ref()) } } }); } - - fn ensure_cache(&self) -> PathBuf { - let url = self.0.load(); - if let Some(p) = url.cache() { - std::fs::create_dir_all(&p).ok(); - p - } else { - url.loc.to_path() - } - } } diff --git a/yazi-fs/src/file.rs b/yazi-fs/src/file.rs index 1d7fc007..f322058f 100644 --- a/yazi-fs/src/file.rs +++ b/yazi-fs/src/file.rs @@ -1,4 +1,4 @@ -use std::{ffi::OsStr, hash::{BuildHasher, Hash, Hasher}, ops::Deref, path::{Path, PathBuf}}; +use std::{ffi::OsStr, hash::{Hash, Hasher}, ops::Deref, path::{Path, PathBuf}}; use yazi_shared::url::{Uri, UrlBuf, UrlLike, Urn}; @@ -25,9 +25,6 @@ impl File { Self { url, cha, link_to: None } } - #[inline] - pub fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) } - #[inline] pub fn chdir(&self, wd: &Path) -> Self { Self { url: self.url.rebase(wd), cha: self.cha, link_to: self.link_to.clone() } diff --git a/yazi-fs/src/hash.rs b/yazi-fs/src/hash.rs new file mode 100644 index 00000000..a9147f99 --- /dev/null +++ b/yazi-fs/src/hash.rs @@ -0,0 +1,47 @@ +use std::hash::{BuildHasher, Hash}; + +use yazi_shared::url::AsUrl; +use yazi_shim::Twox128; + +use crate::{File, cha::Cha}; + +pub trait FsHash64 { + fn hash_u64(&self) -> u64; +} + +impl FsHash64 for File { + fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) } +} + +impl FsHash64 for T { + fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self.as_url()) } +} + +// Hash128 +pub trait FsHash128 { + fn hash_u128(&self) -> u128; +} + +impl FsHash128 for Cha { + fn hash_u128(&self) -> u128 { + let mut h = Twox128::default(); + + self.kind.bits().hash(&mut h); + self.mode.bits().hash(&mut h); + self.len.hash(&mut h); + + self.mtime_dur().ok().map(|d| d.as_nanos()).hash(&mut h); + self.btime_dur().ok().map(|d| d.as_nanos()).hash(&mut h); + self.ctime_dur().ok().map(|d| d.as_nanos()).hash(&mut h); + + h.finish_128() + } +} + +impl FsHash128 for T { + fn hash_u128(&self) -> u128 { + let mut h = Twox128::default(); + self.as_url().hash(&mut h); + h.finish_128() + } +} diff --git a/yazi-fs/src/lib.rs b/yazi-fs/src/lib.rs index 1eeb7c86..eb70639c 100644 --- a/yazi-fs/src/lib.rs +++ b/yazi-fs/src/lib.rs @@ -2,7 +2,7 @@ yazi_macro::mod_pub!(cha error mounts path provider); -yazi_macro::mod_flat!(cwd file files filter fns op sorter sorting splatter stage url xdg); +yazi_macro::mod_flat!(cwd file files filter fns hash op sorter sorting splatter stage url xdg); pub fn init() { CWD.init(<_>::default()); diff --git a/yazi-fs/src/path/mod.rs b/yazi-fs/src/path/mod.rs index cf2daeb9..66986ab4 100644 --- a/yazi-fs/src/path/mod.rs +++ b/yazi-fs/src/path/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(clean expand path relative); +yazi_macro::mod_flat!(clean expand path percent relative); diff --git a/yazi-fs/src/path/percent.rs b/yazi-fs/src/path/percent.rs new file mode 100644 index 00000000..a2c97300 --- /dev/null +++ b/yazi-fs/src/path/percent.rs @@ -0,0 +1,35 @@ +use std::{borrow::Cow, path::{Path, PathBuf}}; + +use percent_encoding::{AsciiSet, CONTROLS, percent_decode, percent_encode}; +use yazi_shared::loc::Loc; + +const SET: &AsciiSet = + &CONTROLS.add(b'"').add(b'*').add(b':').add(b'<').add(b'>').add(b'?').add(b'\\').add(b'|'); + +pub trait PercentEncoding { + fn percent_encode(&self) -> Cow<'_, Path>; + + fn percent_decode(&self) -> Cow<'_, [u8]>; +} + +impl PercentEncoding for Path { + fn percent_encode(&self) -> Cow<'_, Path> { + match percent_encode(self.as_os_str().as_encoded_bytes(), SET).into() { + Cow::Borrowed(_) => self.into(), + Cow::Owned(s) => PathBuf::from(s).into(), + } + } + + fn percent_decode(&self) -> Cow<'_, [u8]> { + match percent_decode(self.as_os_str().as_encoded_bytes()).into() { + Cow::Borrowed(_) => self.as_os_str().as_encoded_bytes().into(), + Cow::Owned(s) => s.into(), + } + } +} + +impl PercentEncoding for Loc<'_> { + fn percent_encode(&self) -> Cow<'_, Path> { self.as_path().percent_encode() } + + fn percent_decode(&self) -> Cow<'_, [u8]> { self.as_path().percent_decode() } +} diff --git a/yazi-fs/src/url.rs b/yazi-fs/src/url.rs index a98322de..462f54b0 100644 --- a/yazi-fs/src/url.rs +++ b/yazi-fs/src/url.rs @@ -1,13 +1,16 @@ use std::{borrow::Cow, ffi::OsStr, path::{Path, PathBuf}}; -use twox_hash::XxHash3_128; -use yazi_shared::{scheme::SchemeRef, url::{AsUrl, Url, UrlBuf, UrlCow}}; +use yazi_shared::{loc::Loc, scheme::SchemeRef, url::{AsUrl, Url, UrlBuf, UrlCow}}; -use crate::Xdg; +use crate::{FsHash128, Xdg, path::PercentEncoding}; pub trait FsUrl<'a> { fn cache(&self) -> Option; + fn cache_lock(&self) -> Option; + + fn cache_root(&self) -> Option; + fn unified_path(self) -> Cow<'a, Path>; fn unified_path_str(self) -> Cow<'a, OsStr> @@ -23,18 +26,37 @@ pub trait FsUrl<'a> { impl<'a> FsUrl<'a> for Url<'a> { fn cache(&self) -> Option { + fn with_loc(loc: Loc, mut path: PathBuf) -> PathBuf { + let mut it = loc.components(); + if it.next() == Some(std::path::Component::RootDir) { + path.push(it.as_path().percent_encode()); + } else { + path.push(".%2F"); + path.push(loc.percent_encode()); + } + path + } + + self.cache_root().map(|root| with_loc(self.loc, root)) + } + + fn cache_lock(&self) -> Option { + self.cache_root().map(|mut root| { + root.push("%lock"); + root.push(format!("{:x}", self.hash_u128())); + root + }) + } + + fn cache_root(&self) -> Option { match self.scheme { SchemeRef::Regular | SchemeRef::Search(_) => None, - SchemeRef::Archive(name) => Some( - Xdg::cache_dir() - .join(format!("archive-{}", yazi_shared::url::Encode::domain(name))) - .join(format!("{:x}", XxHash3_128::oneshot(self.loc.bytes()))), - ), - SchemeRef::Sftp(name) => Some( - Xdg::cache_dir() - .join(format!("sftp-{}", yazi_shared::url::Encode::domain(name))) - .join(format!("{:x}", XxHash3_128::oneshot(self.loc.bytes()))), - ), + SchemeRef::Archive(name) => { + Some(Xdg::cache_dir().join(format!("archive-{}", yazi_shared::url::Encode::domain(name)))) + } + SchemeRef::Sftp(name) => { + Some(Xdg::cache_dir().join(format!("sftp-{}", yazi_shared::url::Encode::domain(name)))) + } } } @@ -46,6 +68,10 @@ impl<'a> FsUrl<'a> for Url<'a> { impl<'a> FsUrl<'a> for UrlBuf { fn cache(&self) -> Option { self.as_url().cache() } + fn cache_lock(&self) -> Option { self.as_url().cache_lock() } + + fn cache_root(&self) -> Option { self.as_url().cache_root() } + fn unified_path(self) -> Cow<'a, Path> { self.cache().unwrap_or_else(|| self.loc.into_path()).into() } @@ -54,6 +80,10 @@ impl<'a> FsUrl<'a> for UrlBuf { impl<'a> FsUrl<'a> for UrlCow<'a> { fn cache(&self) -> Option { self.as_url().cache() } + fn cache_lock(&self) -> Option { self.as_url().cache_lock() } + + fn cache_root(&self) -> Option { self.as_url().cache_root() } + fn unified_path(self) -> Cow<'a, Path> { match (self.cache(), self) { (None, UrlCow::Borrowed { loc, .. }) => loc.as_path().into(), diff --git a/yazi-fs/src/xdg.rs b/yazi-fs/src/xdg.rs index c6f11789..9e977aaa 100644 --- a/yazi-fs/src/xdg.rs +++ b/yazi-fs/src/xdg.rs @@ -1,4 +1,4 @@ -use std::{env, path::PathBuf}; +use std::{env, path::PathBuf, sync::OnceLock}; pub struct Xdg; @@ -43,17 +43,22 @@ impl Xdg { } } - #[inline] - pub fn cache_dir() -> PathBuf { - #[cfg(unix)] - let s = { - use uzers::Users; - format!("yazi-{}", yazi_shared::USERS_CACHE.get_current_uid()) - }; + pub fn cache_dir() -> &'static PathBuf { + static CACHE: OnceLock = OnceLock::new(); - #[cfg(windows)] - let s = "yazi"; + CACHE.get_or_init(|| { + let mut p = env::temp_dir(); + assert!(p.is_absolute(), "Temp dir is not absolute"); - env::temp_dir().join(s) + #[cfg(unix)] + { + use uzers::Users; + p.push(format!("yazi-{}", yazi_shared::USERS_CACHE.get_current_uid())) + } + #[cfg(not(unix))] + p.push("yazi"); + + p + }) } } diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index ba258653..5ee182c4 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -23,6 +23,7 @@ yazi-macro = { path = "../yazi-macro", version = "25.9.15" } yazi-parser = { path = "../yazi-parser", version = "25.9.15" } yazi-proxy = { path = "../yazi-proxy", version = "25.9.15" } yazi-shared = { path = "../yazi-shared", version = "25.9.15" } +yazi-shim = { path = "../yazi-shim", version = "25.9.15" } yazi-vfs = { path = "../yazi-vfs", version = "25.9.15" } yazi-widgets = { path = "../yazi-widgets", version = "25.9.15" } diff --git a/yazi-plugin/src/lib.rs b/yazi-plugin/src/lib.rs index 03fbf6ab..da074117 100644 --- a/yazi-plugin/src/lib.rs +++ b/yazi-plugin/src/lib.rs @@ -2,7 +2,7 @@ yazi_macro::mod_pub!(bindings elements external fs isolate loader process pubsub runtime theme utils); -yazi_macro::mod_flat!(lua twox); +yazi_macro::mod_flat!(lua); pub fn init() -> anyhow::Result<()> { crate::loader::init(); diff --git a/yazi-plugin/src/utils/cache.rs b/yazi-plugin/src/utils/cache.rs index ea6a3692..1b7438aa 100644 --- a/yazi-plugin/src/utils/cache.rs +++ b/yazi-plugin/src/utils/cache.rs @@ -4,9 +4,9 @@ use mlua::{Function, Lua, Table}; use yazi_binding::{FileRef, Url}; use yazi_config::YAZI; use yazi_shared::url::UrlLike; +use yazi_shim::Twox128; use super::Utils; -use crate::Twox128; impl Utils { pub(super) fn file_cache(lua: &Lua) -> mlua::Result { diff --git a/yazi-scheduler/Cargo.toml b/yazi-scheduler/Cargo.toml index 0943a4b3..31228597 100644 --- a/yazi-scheduler/Cargo.toml +++ b/yazi-scheduler/Cargo.toml @@ -22,6 +22,7 @@ yazi-vfs = { path = "../yazi-vfs", version = "25.9.15" } # External dependencies anyhow = { workspace = true } async-priority-channel = "0.2.0" +foldhash = { workspace = true } futures = { workspace = true } hashbrown = { workspace = true } lru = { workspace = true } diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index cd89463b..000fa719 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -1,16 +1,16 @@ -use std::{borrow::Cow, collections::VecDeque}; +use std::{borrow::Cow, collections::VecDeque, hash::{BuildHasher, Hash, Hasher}, path::{Path, PathBuf}}; -use anyhow::{Result, anyhow}; +use anyhow::{Context, Result, anyhow}; use tokio::{io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; use tracing::warn; use yazi_config::YAZI; -use yazi_fs::{cha::Cha, ok_or_not_found, path::{path_relative_to, skip_url}, provider::{DirReader, FileHolder}}; +use yazi_fs::{FsHash128, FsUrl, cha::Cha, ok_or_not_found, path::{path_relative_to, skip_url}, provider::{DirReader, FileHolder, Provider, local::Local}}; use yazi_macro::ok_or_not_found; -use yazi_shared::url::{AsUrl, UrlBuf, UrlCow, UrlLike}; -use yazi_vfs::{VfsCha, copy_with_progress, maybe_exists, provider::{self, DirEntry}}; +use yazi_shared::{timestamp_us, url::{AsUrl, Url, UrlCow, UrlLike}}; +use yazi_vfs::{VfsCha, copy_with_progress, maybe_exists, provider::{self, DirEntry}, unique_name}; use super::{FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash}; -use crate::{LOW, NORMAL, TaskIn, TaskOp, TaskOps, file::{FileOutDelete, FileOutDeleteDo, FileOutHardlink, FileOutHardlinkDo, FileOutLink, FileOutPaste, FileOutPasteDo, FileOutTrash}}; +use crate::{LOW, NORMAL, TaskIn, TaskOp, TaskOps, file::{FileInDownload, FileInUpload, FileOutDelete, FileOutDeleteDo, FileOutDownload, FileOutDownloadDo, FileOutHardlink, FileOutHardlinkDo, FileOutLink, FileOutPaste, FileOutPasteDo, FileOutTrash, FileOutUpload, FileOutUploadDo}}; pub(crate) struct File { ops: TaskOps, @@ -31,7 +31,7 @@ impl File { } if task.cha.is_none() { - task.cha = Some(Self::cha(&task.from, task.follow).await?); + task.cha = Some(Self::cha(&task.from, task.follow, None).await?); } let cha = task.cha.unwrap(); @@ -73,7 +73,7 @@ impl File { let mut it = continue_unless_ok!(provider::read_dir(&src).await); while let Ok(Some(entry)) = it.next().await { let from = entry.url(); - let cha = continue_unless_ok!(Self::entry_cha(entry, &from, task.follow).await); + let cha = continue_unless_ok!(Self::cha(&from, task.follow, Some(entry)).await); if cha.is_dir() { dirs.push_back(from); @@ -154,7 +154,7 @@ impl File { provider::symlink(&src, &task.to, async || { Ok(match task.cha { Some(cha) => cha.is_dir(), - None => Self::cha(&task.from, task.resolve).await?.is_dir(), + None => Self::cha(&task.from, task.resolve, None).await?.is_dir(), }) }) .await?; @@ -167,7 +167,7 @@ impl File { pub(crate) async fn hardlink(&self, mut task: FileInHardlink) -> Result<(), FileOutHardlink> { if task.cha.is_none() { - task.cha = Some(Self::cha(&task.from, task.follow).await?); + task.cha = Some(Self::cha(&task.from, task.follow, None).await?); } let cha = task.cha.unwrap(); @@ -205,7 +205,7 @@ impl File { let mut it = continue_unless_ok!(provider::read_dir(&src).await); while let Ok(Some(entry)) = it.next().await { let from = entry.url(); - let cha = continue_unless_ok!(Self::entry_cha(entry, &from, task.follow).await); + let cha = continue_unless_ok!(Self::cha(&from, task.follow, Some(entry)).await); if cha.is_dir() { dirs.push_back(from); @@ -288,20 +288,188 @@ impl File { Ok(self.ops.out(task.id, FileOutTrash::Succ)) } - #[inline] - async fn cha(url: impl AsUrl, follow: bool) -> io::Result { - let url = url.as_url(); - let cha = provider::symlink_metadata(url).await?; + pub(crate) async fn download(&self, mut task: FileInDownload) -> Result<(), FileOutDownload> { + if task.cha.is_none() { + task.cha = Some(Self::cha(&task.url, true, None).await?); + } + + let cha = task.cha.unwrap(); + if cha.is_orphan() { + Err(io::Error::new(NotFound, "Source of symlink doesn't exist"))?; + } + + if !cha.is_dir() { + let id = task.id; + self.ops.out(id, FileOutDownload::New(cha.len)); + self.queue(task, LOW); + return Ok(self.ops.out(id, FileOutDownload::Succ)); + } + + macro_rules! continue_unless_ok { + ($result:expr) => { + match $result { + Ok(v) => v, + Err(e) => { + self.ops.out(task.id, FileOutDownload::Deform(e.to_string())); + continue; + } + } + }; + } + + let mut dirs = VecDeque::from([task.url.clone()]); + while let Some(src) = dirs.pop_front() { + let cache = continue_unless_ok!(src.cache().ok_or("Cannot determine cache path")); + continue_unless_ok!(match Local.create_dir(&cache).await { + Err(e) if e.kind() != AlreadyExists => Err(e), + _ => Ok(()), + }); + + let mut it = continue_unless_ok!(provider::read_dir(&src).await); + while let Ok(Some(entry)) = it.next().await { + let from = entry.url(); + let cha = continue_unless_ok!(Self::cha(&from, true, Some(entry)).await); + + if cha.is_orphan() { + continue_unless_ok!(Err("Source of symlink doesn't exist")); + } else if cha.is_dir() { + dirs.push_back(from); + } else { + self.ops.out(task.id, FileOutDownload::New(cha.len)); + self.queue(task.spawn(from, cha), LOW); + } + } + } + + Ok(self.ops.out(task.id, FileOutDownload::Succ)) + } + + pub(crate) async fn download_do( + &self, + mut task: FileInDownload, + ) -> Result<(), FileOutDownloadDo> { + let cha = task.cha.unwrap(); + + let cache = task.url.cache().context("Cannot determine cache path")?; + let cache_tmp = Self::tmp(&cache).await?; + + let mut it = copy_with_progress(&task.url, Url::regular(&cache_tmp), cha); + while let Some(res) = it.recv().await { + match res { + Ok(0) => { + Local.rename(&cache_tmp, &cache).await?; + + let lock = task.url.cache_lock().context("Cannot determine cache lock")?; + Local.write(lock, format!("{:x}", cha.hash_u128())).await?; + + break; + } + Ok(n) => self.ops.out(task.id, FileOutDownloadDo::Adv(n)), + Err(e) if e.kind() == NotFound => { + warn!("Download task partially done: {task:?}"); + break; + } + // Operation not permitted (os error 1) + // Attribute not found (os error 93) + Err(e) + if task.retry < YAZI.tasks.bizarre_retry + && matches!(e.raw_os_error(), Some(1) | Some(93)) => + { + task.retry += 1; + self.ops.out(task.id, FileOutDownloadDo::Log(format!("Retrying due to error: {e}"))); + return Ok(self.queue(task, LOW)); + } + Err(e) => Err(e)?, + } + } + Ok(self.ops.out(task.id, FileOutDownloadDo::Succ)) + } + + pub(crate) async fn upload(&self, mut task: FileInUpload) -> Result<(), FileOutUpload> { + todo!(); + // if task.cha.is_none() { + // task.cha = Some(Self::cha(Url::regular(&task.path), true, None).await?); + // } + + // let cha = task.cha.unwrap(); + // if cha.is_orphan() { + // Err(io::Error::new(NotFound, "Source of symlink doesn't exist"))?; + // } + + // if !cha.is_dir() { + // let id = task.id; + // self.ops.out(id, FileOutUpload::New(cha.len)); + // self.queue(task, LOW); + // return Ok(self.ops.out(id, FileOutUpload::Succ)); + // } + + // macro_rules! continue_unless_ok { + // ($result:expr) => { + // match $result { + // Ok(v) => v, + // Err(e) => { + // self.ops.out(task.id, FileOutUpload::Deform(e.to_string())); + // continue; + // } + // } + // }; + // } + + // let mut dirs = VecDeque::from([task.path.clone()]); + // while let Some(src) = dirs.pop_front() { + // let cache = continue_unless_ok!(src.cache().ok_or("Cannot determine + // cache path")); continue_unless_ok!(match + // Local.create_dir(&cache).await { Err(e) if e.kind() != AlreadyExists + // => Err(e), _ => Ok(()), + // }); + + // let mut it = continue_unless_ok!(provider::read_dir(&src).await); + // while let Ok(Some(entry)) = it.next().await { + // let from = entry.url(); + // let cha = continue_unless_ok!(Self::cha(&from, true, + // Some(entry)).await); + + // if cha.is_orphan() { + // continue_unless_ok!(Err("Source of symlink doesn't exist")); + // } else if cha.is_dir() { + // dirs.push_back(from); + // } else { + // self.ops.out(task.id, FileOutUpload::New(cha.len)); + // self.queue(task.spawn(from, cha), LOW); + // } + // } + // } + + // Ok(self.ops.out(task.id, FileOutUpload::Succ)) + } + + pub(crate) async fn upload_do(&self, mut task: FileInUpload) -> Result<(), FileOutUploadDo> { + todo!() + } + + async fn cha(url: U, follow: bool, entry: Option) -> io::Result + where + U: AsUrl, + { + let cha = if let Some(entry) = entry { + entry.metadata().await? + } else { + provider::symlink_metadata(url.as_url()).await? + }; Ok(if follow { Cha::from_follow(url, cha).await } else { cha }) } - #[inline] - async fn entry_cha(entry: DirEntry, url: &UrlBuf, follow: bool) -> io::Result { - Ok(if follow { - Cha::from_follow(url, entry.metadata().await?).await - } else { - entry.metadata().await? - }) + async fn tmp(path: &Path) -> io::Result { + let Some(parent) = path.parent() else { + Err(io::Error::new(io::ErrorKind::InvalidInput, "Path has no parent"))? + }; + + let mut h = foldhash::fast::FixedState::default().build_hasher(); + path.hash(&mut h); + timestamp_us().hash(&mut h); + + let u = parent.join(format!(".{:x}.%tmp", h.finish())).into(); + Ok(unique_name(u, async { false }).await?.into_path().expect("a path")) } } diff --git a/yazi-scheduler/src/file/in.rs b/yazi-scheduler/src/file/in.rs index 55fe1fd7..7b4069db 100644 --- a/yazi-scheduler/src/file/in.rs +++ b/yazi-scheduler/src/file/in.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use yazi_fs::cha::Cha; use yazi_shared::{Id, url::UrlBuf}; @@ -81,3 +83,32 @@ pub(crate) struct FileInTrash { pub(crate) id: Id, pub(crate) target: UrlBuf, } + +// --- Download +#[derive(Clone, Debug)] +pub(crate) struct FileInDownload { + pub(crate) id: Id, + pub(crate) url: UrlBuf, + pub(crate) cha: Option, + pub(crate) retry: u8, +} + +impl FileInDownload { + pub(super) fn spawn(&self, url: UrlBuf, cha: Cha) -> Self { + Self { id: self.id, url, cha: Some(cha), retry: self.retry } + } +} + +// --- Upload +#[derive(Clone, Debug)] +pub(crate) struct FileInUpload { + pub(crate) id: Id, + pub(crate) path: PathBuf, + pub(crate) cha: Option, +} + +impl FileInUpload { + pub(super) fn spawn(&self, path: PathBuf, cha: Cha) -> Self { + Self { id: self.id, path, cha: Some(cha) } + } +} diff --git a/yazi-scheduler/src/file/out.rs b/yazi-scheduler/src/file/out.rs index 39409521..95bdb090 100644 --- a/yazi-scheduler/src/file/out.rs +++ b/yazi-scheduler/src/file/out.rs @@ -265,3 +265,153 @@ impl FileOutTrash { } } } + +// --- Download +#[derive(Debug)] +pub(crate) enum FileOutDownload { + New(u64), + Deform(String), + Succ, + Fail(String), +} + +impl From for FileOutDownload { + fn from(value: std::io::Error) -> Self { Self::Fail(value.to_string()) } +} + +impl FileOutDownload { + pub(crate) fn reduce(self, task: &mut Task) { + let TaskProg::FileDownload(prog) = &mut task.prog else { return }; + match self { + Self::New(bytes) => { + prog.total_files += 1; + prog.total_bytes += bytes; + } + Self::Deform(reason) => { + prog.total_files += 1; + prog.failed_files += 1; + task.log(reason); + } + Self::Succ => { + prog.collected = Some(true); + } + Self::Fail(reason) => { + prog.collected = Some(false); + task.log(reason); + } + } + } +} + +// --- DownloadDo +#[derive(Debug)] +pub(crate) enum FileOutDownloadDo { + Adv(u64), + Log(String), + Succ, + Fail(String), +} + +impl From for FileOutDownloadDo { + fn from(value: std::io::Error) -> Self { Self::Fail(value.to_string()) } +} + +impl From for FileOutDownloadDo { + fn from(value: anyhow::Error) -> Self { Self::Fail(value.to_string()) } +} + +impl FileOutDownloadDo { + pub(crate) fn reduce(self, task: &mut Task) { + let TaskProg::FileDownload(prog) = &mut task.prog else { return }; + match self { + Self::Adv(size) => { + prog.processed_bytes += size; + } + Self::Log(line) => { + task.log(line); + } + Self::Succ => { + prog.success_files += 1; + } + Self::Fail(reason) => { + prog.failed_files += 1; + task.log(reason); + } + } + } +} + +// --- Upload +#[derive(Debug)] +pub(crate) enum FileOutUpload { + New(u64), + Deform(String), + Succ, + Fail(String), +} + +impl From for FileOutUpload { + fn from(value: std::io::Error) -> Self { Self::Fail(value.to_string()) } +} + +impl FileOutUpload { + pub(crate) fn reduce(self, task: &mut Task) { + let TaskProg::FileUpload(prog) = &mut task.prog else { return }; + match self { + Self::New(bytes) => { + prog.total_files += 1; + prog.total_bytes += bytes; + } + Self::Deform(reason) => { + prog.total_files += 1; + prog.failed_files += 1; + task.log(reason); + } + Self::Succ => { + prog.collected = Some(true); + } + Self::Fail(reason) => { + prog.collected = Some(false); + task.log(reason); + } + } + } +} + +// --- UploadDo +#[derive(Debug)] +pub(crate) enum FileOutUploadDo { + Adv(u64), + Log(String), + Succ, + Fail(String), +} + +impl From for FileOutUploadDo { + fn from(value: std::io::Error) -> Self { Self::Fail(value.to_string()) } +} + +impl From for FileOutUploadDo { + fn from(value: anyhow::Error) -> Self { Self::Fail(value.to_string()) } +} + +impl FileOutUploadDo { + pub(crate) fn reduce(self, task: &mut Task) { + let TaskProg::FileUpload(prog) = &mut task.prog else { return }; + match self { + Self::Adv(size) => { + prog.processed_bytes += size; + } + Self::Log(line) => { + task.log(line); + } + Self::Succ => { + prog.success_files += 1; + } + Self::Fail(reason) => { + prog.failed_files += 1; + task.log(reason); + } + } + } +} diff --git a/yazi-scheduler/src/file/progress.rs b/yazi-scheduler/src/file/progress.rs index 61a44efe..2238d922 100644 --- a/yazi-scheduler/src/file/progress.rs +++ b/yazi-scheduler/src/file/progress.rs @@ -190,3 +190,99 @@ impl FileProgTrash { pub fn percent(self) -> Option { None } } + +// --- Download +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)] +pub struct FileProgDownload { + pub total_files: u32, + pub success_files: u32, + pub failed_files: u32, + pub total_bytes: u64, + pub processed_bytes: u64, + pub collected: Option, +} + +impl From for TaskSummary { + fn from(value: FileProgDownload) -> Self { + Self { + total: value.total_files, + success: value.success_files, + failed: value.failed_files, + percent: value.percent().map(Into::into), + } + } +} + +impl FileProgDownload { + pub fn running(self) -> bool { + self.collected.is_none() || self.success_files + self.failed_files != self.total_files + } + + pub fn success(self) -> bool { + self.collected == Some(true) && self.success_files == self.total_files + } + + pub fn failed(self) -> bool { self.collected == Some(false) } + + pub fn cleaned(self) -> bool { false } + + pub fn percent(self) -> Option { + Some(if self.success() { + 100.0 + } else if self.failed() { + 0.0 + } else if self.total_bytes != 0 { + 99.99f32.min(self.processed_bytes as f32 / self.total_bytes as f32 * 100.0) + } else { + 99.99 + }) + } +} + +// --- Upload +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)] +pub struct FileProgUpload { + pub total_files: u32, + pub success_files: u32, + pub failed_files: u32, + pub total_bytes: u64, + pub processed_bytes: u64, + pub collected: Option, +} + +impl From for TaskSummary { + fn from(value: FileProgUpload) -> Self { + Self { + total: value.total_files, + success: value.success_files, + failed: value.failed_files, + percent: value.percent().map(Into::into), + } + } +} + +impl FileProgUpload { + pub fn running(self) -> bool { + self.collected.is_none() || self.success_files + self.failed_files != self.total_files + } + + pub fn success(self) -> bool { + self.collected == Some(true) && self.success_files == self.total_files + } + + pub fn failed(self) -> bool { self.collected == Some(false) } + + pub fn cleaned(self) -> bool { false } + + pub fn percent(self) -> Option { + Some(if self.success() { + 100.0 + } else if self.failed() { + 0.0 + } else if self.total_bytes != 0 { + 99.99f32.min(self.processed_bytes as f32 / self.total_bytes as f32 * 100.0) + } else { + 99.99 + }) + } +} diff --git a/yazi-scheduler/src/in.rs b/yazi-scheduler/src/in.rs index 19190bc9..57605e47 100644 --- a/yazi-scheduler/src/in.rs +++ b/yazi-scheduler/src/in.rs @@ -1,6 +1,6 @@ use yazi_shared::Id; -use crate::{file::{FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash}, impl_from_in, plugin::PluginInEntry, prework::{PreworkInFetch, PreworkInLoad, PreworkInSize}}; +use crate::{file::{FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInPaste, FileInTrash, FileInUpload}, impl_from_in, plugin::PluginInEntry, prework::{PreworkInFetch, PreworkInLoad, PreworkInSize}}; #[derive(Debug)] pub(crate) enum TaskIn { @@ -10,6 +10,8 @@ pub(crate) enum TaskIn { FileHardlink(FileInHardlink), FileDelete(FileInDelete), FileTrash(FileInTrash), + FileDownload(FileInDownload), + FileUpload(FileInUpload), // Plugin PluginEntry(PluginInEntry), @@ -22,7 +24,7 @@ pub(crate) enum TaskIn { impl_from_in! { // File - FilePaste(FileInPaste), FileLink(FileInLink), FileHardlink(FileInHardlink), FileDelete(FileInDelete), FileTrash(FileInTrash), + FilePaste(FileInPaste), FileLink(FileInLink), FileHardlink(FileInHardlink), FileDelete(FileInDelete), FileTrash(FileInTrash), FileDownload(FileInDownload), FileUpload(FileInUpload), // Plugin PluginEntry(PluginInEntry), // Prework @@ -38,10 +40,10 @@ impl TaskIn { Self::FileHardlink(r#in) => r#in.id, Self::FileDelete(r#in) => r#in.id, Self::FileTrash(r#in) => r#in.id, - + Self::FileDownload(r#in) => r#in.id, + Self::FileUpload(r#in) => r#in.id, // Plugin Self::PluginEntry(r#in) => r#in.id, - // Prework Self::PreworkFetch(r#in) => r#in.id, Self::PreworkLoad(r#in) => r#in.id, diff --git a/yazi-scheduler/src/out.rs b/yazi-scheduler/src/out.rs index b9928567..cd4b687f 100644 --- a/yazi-scheduler/src/out.rs +++ b/yazi-scheduler/src/out.rs @@ -1,4 +1,4 @@ -use crate::{Task, file::{FileOutDelete, FileOutDeleteDo, FileOutHardlink, FileOutHardlinkDo, FileOutLink, FileOutPaste, FileOutPasteDo, FileOutTrash}, impl_from_out, plugin::PluginOutEntry, prework::{PreworkOutFetch, PreworkOutLoad, PreworkOutSize}, process::{ProcessOutBg, ProcessOutBlock, ProcessOutOrphan}}; +use crate::{Task, file::{FileOutDelete, FileOutDeleteDo, FileOutDownload, FileOutDownloadDo, FileOutHardlink, FileOutHardlinkDo, FileOutLink, FileOutPaste, FileOutPasteDo, FileOutTrash, FileOutUpload, FileOutUploadDo}, impl_from_out, plugin::PluginOutEntry, prework::{PreworkOutFetch, PreworkOutLoad, PreworkOutSize}, process::{ProcessOutBg, ProcessOutBlock, ProcessOutOrphan}}; #[derive(Debug)] pub(super) enum TaskOut { @@ -11,6 +11,10 @@ pub(super) enum TaskOut { FileDelete(FileOutDelete), FileDeleteDo(FileOutDeleteDo), FileTrash(FileOutTrash), + FileDownload(FileOutDownload), + FileDownloadDo(FileOutDownloadDo), + FileUpload(FileOutUpload), + FileUploadDo(FileOutUploadDo), // Plugin PluginEntry(PluginOutEntry), @@ -28,7 +32,7 @@ pub(super) enum TaskOut { impl_from_out! { // File - FilePaste(FileOutPaste), FilePasteDo(FileOutPasteDo), FileLink(FileOutLink), FileHardlink(FileOutHardlink), FileHardlinkDo(FileOutHardlinkDo), FileDelete(FileOutDelete), FileDeleteDo(FileOutDeleteDo), FileTrash(FileOutTrash), + FilePaste(FileOutPaste), FilePasteDo(FileOutPasteDo), FileLink(FileOutLink), FileHardlink(FileOutHardlink), FileHardlinkDo(FileOutHardlinkDo), FileDelete(FileOutDelete), FileDeleteDo(FileOutDeleteDo), FileTrash(FileOutTrash), FileDownload(FileOutDownload), FileDownloadDo(FileOutDownloadDo), FileUpload(FileOutUpload), FileUploadDo(FileOutUploadDo), // Plugin PluginEntry(PluginOutEntry), // Prework @@ -49,6 +53,10 @@ impl TaskOut { Self::FileDelete(out) => out.reduce(task), Self::FileDeleteDo(out) => out.reduce(task), Self::FileTrash(out) => out.reduce(task), + Self::FileDownload(out) => out.reduce(task), + Self::FileDownloadDo(out) => out.reduce(task), + Self::FileUpload(out) => out.reduce(task), + Self::FileUploadDo(out) => out.reduce(task), // Plugin Self::PluginEntry(out) => out.reduce(task), // Prework diff --git a/yazi-scheduler/src/prework/prework.rs b/yazi-scheduler/src/prework/prework.rs index 1ba4863d..209a2728 100644 --- a/yazi-scheduler/src/prework/prework.rs +++ b/yazi-scheduler/src/prework/prework.rs @@ -8,7 +8,7 @@ use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; use tracing::error; use yazi_config::Priority; -use yazi_fs::FilesOp; +use yazi_fs::{FilesOp, FsHash64}; use yazi_plugin::isolate; use yazi_shared::{event::CmdCow, url::{UrlBuf, UrlLike}}; use yazi_vfs::provider; diff --git a/yazi-scheduler/src/process/shell.rs b/yazi-scheduler/src/process/shell.rs index f4e131ee..24f04366 100644 --- a/yazi-scheduler/src/process/shell.rs +++ b/yazi-scheduler/src/process/shell.rs @@ -1,9 +1,9 @@ -use std::{borrow::Cow, ffi::OsString, process::Stdio}; +use std::{ffi::OsString, process::Stdio}; -use anyhow::{Result, bail}; +use anyhow::Result; use tokio::process::{Child, Command}; -use yazi_fs::FsUrl; -use yazi_shared::url::{UrlBuf, UrlCow, UrlLike}; +use yazi_fs::{Cwd, FsUrl}; +use yazi_shared::url::{AsUrl, UrlBuf, UrlCow}; pub(crate) struct ShellOpt { pub(crate) cwd: UrlBuf, @@ -28,14 +28,7 @@ impl ShellOpt { pub(crate) async fn shell(opt: ShellOpt) -> Result { tokio::task::spawn_blocking(move || { - let cwd: Cow<_> = if let Some(path) = opt.cwd.as_path() { - path.into() - } else if let Some(cache) = opt.cwd.cache() { - std::fs::create_dir_all(&cache).ok(); - cache.into() - } else { - bail!("failed to determine a working directory"); - }; + let cwd = Cwd::ensure(opt.cwd.as_url()); #[cfg(unix)] return Ok(unsafe { diff --git a/yazi-scheduler/src/progress.rs b/yazi-scheduler/src/progress.rs index 60536fe1..10abc5fc 100644 --- a/yazi-scheduler/src/progress.rs +++ b/yazi-scheduler/src/progress.rs @@ -1,7 +1,7 @@ use serde::Serialize; use yazi_parser::app::TaskSummary; -use crate::{file::{FileProgDelete, FileProgHardlink, FileProgLink, FileProgPaste, FileProgTrash}, impl_from_prog, plugin::PluginProgEntry, prework::{PreworkProgFetch, PreworkProgLoad, PreworkProgSize}, process::{ProcessProgBg, ProcessProgBlock, ProcessProgOrphan}}; +use crate::{file::{FileProgDelete, FileProgDownload, FileProgHardlink, FileProgLink, FileProgPaste, FileProgTrash, FileProgUpload}, impl_from_prog, plugin::PluginProgEntry, prework::{PreworkProgFetch, PreworkProgLoad, PreworkProgSize}, process::{ProcessProgBg, ProcessProgBlock, ProcessProgOrphan}}; #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] #[serde(tag = "kind")] @@ -12,6 +12,8 @@ pub enum TaskProg { FileHardlink(FileProgHardlink), FileDelete(FileProgDelete), FileTrash(FileProgTrash), + FileDownload(FileProgDownload), + FileUpload(FileProgUpload), // Plugin PluginEntry(PluginProgEntry), // Prework @@ -26,7 +28,7 @@ pub enum TaskProg { impl_from_prog! { // File - FilePaste(FileProgPaste), FileLink(FileProgLink), FileHardlink(FileProgHardlink), FileDelete(FileProgDelete), FileTrash(FileProgTrash), + FilePaste(FileProgPaste), FileLink(FileProgLink), FileHardlink(FileProgHardlink), FileDelete(FileProgDelete), FileTrash(FileProgTrash), FileDownload(FileProgDownload), FileUpload(FileProgUpload), // Plugin PluginEntry(PluginProgEntry), // Prework @@ -44,6 +46,8 @@ impl From for TaskSummary { TaskProg::FileHardlink(p) => p.into(), TaskProg::FileDelete(p) => p.into(), TaskProg::FileTrash(p) => p.into(), + TaskProg::FileDownload(p) => p.into(), + TaskProg::FileUpload(p) => p.into(), // Plugin TaskProg::PluginEntry(p) => p.into(), // Prework @@ -67,6 +71,8 @@ impl TaskProg { Self::FileHardlink(p) => p.running(), Self::FileDelete(p) => p.running(), Self::FileTrash(p) => p.running(), + Self::FileDownload(p) => p.running(), + Self::FileUpload(p) => p.running(), // Plugin Self::PluginEntry(p) => p.running(), // Prework @@ -88,6 +94,8 @@ impl TaskProg { Self::FileHardlink(p) => p.success(), Self::FileDelete(p) => p.success(), Self::FileTrash(p) => p.success(), + Self::FileDownload(p) => p.success(), + Self::FileUpload(p) => p.success(), // Plugin Self::PluginEntry(p) => p.success(), // Prework @@ -109,6 +117,8 @@ impl TaskProg { Self::FileHardlink(p) => p.failed(), Self::FileDelete(p) => p.failed(), Self::FileTrash(p) => p.failed(), + Self::FileDownload(p) => p.failed(), + Self::FileUpload(p) => p.failed(), // Plugin Self::PluginEntry(p) => p.failed(), // Prework @@ -130,6 +140,8 @@ impl TaskProg { Self::FileHardlink(p) => p.cleaned(), Self::FileDelete(p) => p.cleaned(), Self::FileTrash(p) => p.cleaned(), + Self::FileDownload(p) => p.cleaned(), + Self::FileUpload(p) => p.cleaned(), // Plugin Self::PluginEntry(p) => p.cleaned(), // Prework @@ -151,6 +163,8 @@ impl TaskProg { Self::FileHardlink(p) => p.percent(), Self::FileDelete(p) => p.percent(), Self::FileTrash(p) => p.percent(), + Self::FileDownload(p) => p.percent(), + Self::FileUpload(p) => p.percent(), // Plugin Self::PluginEntry(p) => p.percent(), // Prework @@ -172,6 +186,8 @@ impl TaskProg { Self::FileHardlink(_) => true, Self::FileDelete(_) => true, Self::FileTrash(_) => true, + Self::FileDownload(_) => true, + Self::FileUpload(_) => true, // Plugin Self::PluginEntry(_) => true, // Prework diff --git a/yazi-scheduler/src/scheduler.rs b/yazi-scheduler/src/scheduler.rs index e7bedd54..c68d3a28 100644 --- a/yazi-scheduler/src/scheduler.rs +++ b/yazi-scheduler/src/scheduler.rs @@ -13,7 +13,7 @@ use yazi_shared::{Id, Throttle, url::{UrlBuf, UrlLike}}; use yazi_vfs::{must_be_dir, provider, unique_name}; use super::{Ongoing, TaskOp}; -use crate::{HIGH, LOW, NORMAL, TaskIn, TaskOps, TaskOut, file::{File, FileInDelete, FileInHardlink, FileInLink, FileInPaste, FileInTrash, FileOutDelete, FileOutHardlink, FileOutPaste, FileProgDelete, FileProgHardlink, FileProgLink, FileProgPaste, FileProgTrash}, plugin::{Plugin, PluginInEntry, PluginProgEntry}, prework::{Prework, PreworkInFetch, PreworkInLoad, PreworkInSize, PreworkProgFetch, PreworkProgLoad, PreworkProgSize}, process::{Process, ProcessInBg, ProcessInBlock, ProcessInOrphan, ProcessOutBg, ProcessOutBlock, ProcessOutOrphan, ProcessProgBg, ProcessProgBlock, ProcessProgOrphan}}; +use crate::{HIGH, LOW, NORMAL, TaskIn, TaskOps, TaskOut, file::{File, FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInPaste, FileInTrash, FileOutDelete, FileOutDownload, FileOutHardlink, FileOutPaste, FileProgDelete, FileProgDownload, FileProgHardlink, FileProgLink, FileProgPaste, FileProgTrash}, plugin::{Plugin, PluginInEntry, PluginProgEntry}, prework::{Prework, PreworkInFetch, PreworkInLoad, PreworkInSize, PreworkProgFetch, PreworkProgLoad, PreworkProgSize}, process::{Process, ProcessInBg, ProcessInBlock, ProcessInOrphan, ProcessOutBg, ProcessOutBlock, ProcessOutOrphan, ProcessProgBg, ProcessProgBlock, ProcessProgOrphan}}; pub struct Scheduler { file: Arc, @@ -208,21 +208,21 @@ impl Scheduler { self.send_micro(id, LOW, async move { file.trash(FileInTrash { id, target }) }) } - pub fn file_download(&self, from: UrlBuf, done: Option>) { + pub fn file_download(&self, url: UrlBuf, done: Option>) { let mut ongoing = self.ongoing.lock(); - let id = ongoing.add::(format!("Download {}", from.display())); + let id = ongoing.add::(format!("Download {}", url.display())); if let Some(tx) = done { ongoing.hooks.add_sync(id, move |canceled| _ = tx.send(canceled)); } - let Some(to) = from.cache().map(UrlBuf::from) else { - return self.ops.out(id, FileOutPaste::Fail("Cannot download non-remote file".to_owned())); + if !url.scheme.is_virtual() { + return self.ops.out(id, FileOutDownload::Fail("Cannot download non-remote file".to_owned())); }; let file = self.file.clone(); self.send_micro(id, LOW, async move { - file.paste(FileInPaste { id, from, to, cha: None, cut: false, follow: false, retry: 0 }).await + file.download(FileInDownload { id, url, cha: None, retry: 0 }).await }); } @@ -397,6 +397,8 @@ impl Scheduler { TaskIn::FileHardlink(r#in) => file.hardlink_do(r#in).await.map_err(Into::into), TaskIn::FileDelete(r#in) => file.delete_do(r#in).await.map_err(Into::into), TaskIn::FileTrash(r#in) => file.trash_do(r#in).await.map_err(Into::into), + TaskIn::FileDownload(r#in) => file.download_do(r#in).await.map_err(Into::into), + TaskIn::FileUpload(r#in) => file.upload_do(r#in).await.map_err(Into::into), // Plugin TaskIn::PluginEntry(r#in) => plugin.macro_do(r#in).await.map_err(Into::into), // Prework diff --git a/yazi-sftp/Cargo.toml b/yazi-sftp/Cargo.toml index 32a3895b..dcd6bb41 100644 --- a/yazi-sftp/Cargo.toml +++ b/yazi-sftp/Cargo.toml @@ -14,6 +14,3 @@ parking_lot = { workspace = true } russh = { workspace = true } serde = { workspace = true } tokio = { workspace = true } - -[target."cfg(windows)".dependencies] -os_str_bytes = { version = "7.1.1", default-features = false, features = [ "conversions" ] } diff --git a/yazi-sftp/src/byte_str.rs b/yazi-sftp/src/byte_str.rs index e546ab5e..db545ac5 100644 --- a/yazi-sftp/src/byte_str.rs +++ b/yazi-sftp/src/byte_str.rs @@ -2,6 +2,8 @@ use std::{borrow::Cow, ffi::{OsStr, OsString}, ops::Deref, path::{Path, PathBuf} use serde::{Deserialize, Serialize}; +use crate::Error; + #[derive(Debug, Default, Deserialize, Serialize)] pub struct ByteStr<'a>(Cow<'a, [u8]>); @@ -19,32 +21,6 @@ impl<'a> From<&'a ByteStr<'a>> for ByteStr<'a> { fn from(value: &'a ByteStr) -> Self { ByteStr(Cow::Borrowed(&value.0)) } } -impl<'a> From<&'a OsStr> for ByteStr<'a> { - fn from(value: &'a OsStr) -> Self { - #[cfg(unix)] - { - use std::os::unix::ffi::OsStrExt; - ByteStr(Cow::Borrowed(value.as_bytes())) - } - #[cfg(windows)] - { - use os_str_bytes::OsStrBytes; - ByteStr(value.to_raw_bytes()) - } - } -} - -impl<'a> From<&'a Path> for ByteStr<'a> { - fn from(value: &'a Path) -> Self { ByteStr::from(value.as_os_str()) } -} - -impl<'a, T> From<&'a T> for ByteStr<'a> -where - T: AsRef, -{ - fn from(value: &'a T) -> Self { Self::from(value.as_ref()) } -} - impl PartialEq<&str> for ByteStr<'_> { fn eq(&self, other: &&str) -> bool { self.0 == other.as_bytes() } } @@ -58,8 +34,7 @@ impl<'a> ByteStr<'a> { } #[cfg(windows)] { - use os_str_bytes::OsStrBytes; - OsStr::assert_from_raw_bytes(self.0.as_ref()) + super::wtf::bytes_to_wide(self.0.as_ref()) } } @@ -71,8 +46,10 @@ impl<'a> ByteStr<'a> { } #[cfg(windows)] { - use os_str_bytes::OsStrBytes; - OsStr::assert_from_raw_bytes(self.0).into_owned() + match super::wtf::bytes_to_wide(self.0.as_ref()) { + Cow::Borrowed(_) => unsafe { String::from_utf8_unchecked(self.0.into_owned()) }.into(), + Cow::Owned(s) => s, + } } } @@ -107,3 +84,31 @@ impl<'a> ByteStr<'a> { Self(Cow::Borrowed(bytes)) } } + +// --- Traits +pub trait ToByteStr<'a> { + fn to_byte_str(self) -> Result, Error>; +} + +impl<'a, T> ToByteStr<'a> for &'a T +where + T: AsRef + ?Sized, +{ + fn to_byte_str(self) -> Result, Error> { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + Ok(ByteStr(Cow::Borrowed(self.as_ref().as_os_str().as_bytes()))) + } + #[cfg(windows)] + { + super::wtf::wide_to_bytes(self.as_ref().as_os_str()) + .ok_or(Error::custom("failed to convert wide path to bytes")) + .map(ByteStr) + } + } +} + +impl<'a> ToByteStr<'a> for &'a ByteStr<'a> { + fn to_byte_str(self) -> Result, Error> { Ok(ByteStr(Cow::Borrowed(&self.0))) } +} diff --git a/yazi-sftp/src/lib.rs b/yazi-sftp/src/lib.rs index 845a865a..e7e7b9d2 100644 --- a/yazi-sftp/src/lib.rs +++ b/yazi-sftp/src/lib.rs @@ -13,6 +13,8 @@ mod operator; mod packet; mod ser; mod session; +#[cfg(windows)] +mod wtf; pub use byte_str::*; pub(crate) use de::*; diff --git a/yazi-sftp/src/operator.rs b/yazi-sftp/src/operator.rs index 58557aea..595d6db8 100644 --- a/yazi-sftp/src/operator.rs +++ b/yazi-sftp/src/operator.rs @@ -3,7 +3,7 @@ use std::{ops::Deref, path::PathBuf, sync::Arc}; use russh::{ChannelStream, client::Msg}; use tokio::sync::oneshot; -use crate::{ByteStr, Error, Packet, Session, fs::{Attrs, File, Flags, ReadDir}, requests, responses}; +use crate::{ByteStr, Error, Packet, Session, ToByteStr, fs::{Attrs, File, Flags, ReadDir}, requests, responses}; pub struct Operator(Arc); @@ -28,9 +28,9 @@ impl Operator { pub async fn open<'a, P>(&self, path: P, flags: Flags, attrs: &'a Attrs) -> Result where - P: Into>, + P: ToByteStr<'a>, { - let handle: responses::Handle = self.send(requests::Open::new(path, flags, attrs)).await?; + let handle: responses::Handle = self.send(requests::Open::new(path, flags, attrs)?).await?; Ok(File::new(&self.0, handle.handle)) } @@ -59,9 +59,9 @@ impl Operator { pub async fn lstat<'a, P>(&self, path: P) -> Result where - P: Into>, + P: ToByteStr<'a>, { - let attrs: responses::Attrs = self.send(requests::Lstat::new(path)).await?; + let attrs: responses::Attrs = self.send(requests::Lstat::new(path)?).await?; Ok(attrs.attrs) } @@ -72,9 +72,9 @@ impl Operator { pub async fn setstat<'a, P>(&self, path: P, attrs: Attrs) -> Result<(), Error> where - P: Into>, + P: ToByteStr<'a>, { - let status: responses::Status = self.send(requests::SetStat::new(path, attrs)).await?; + let status: responses::Status = self.send(requests::SetStat::new(path, attrs)?).await?; status.into() } @@ -83,42 +83,45 @@ impl Operator { status.into() } - pub async fn read_dir<'a>(&'a self, dir: impl Into>) -> Result { - let dir: ByteStr = dir.into(); - let handle: responses::Handle = self.send(requests::OpenDir::new(&dir)).await?; + pub async fn read_dir<'a, P>(&'a self, dir: P) -> Result + where + P: ToByteStr<'a>, + { + let dir: ByteStr = dir.to_byte_str()?; + let handle: responses::Handle = self.send(requests::OpenDir::new(&dir)?).await?; Ok(ReadDir::new(&self.0, dir, handle.handle)) } pub async fn remove<'a, P>(&self, path: P) -> Result<(), Error> where - P: Into>, + P: ToByteStr<'a>, { - let status: responses::Status = self.send(requests::Remove::new(path)).await?; + let status: responses::Status = self.send(requests::Remove::new(path)?).await?; status.into() } pub async fn mkdir<'a, P>(&self, path: P, attrs: Attrs) -> Result<(), Error> where - P: Into>, + P: ToByteStr<'a>, { - let status: responses::Status = self.send(requests::Mkdir::new(path, attrs)).await?; + let status: responses::Status = self.send(requests::Mkdir::new(path, attrs)?).await?; status.into() } pub async fn rmdir<'a, P>(&self, path: P) -> Result<(), Error> where - P: Into>, + P: ToByteStr<'a>, { - let status: responses::Status = self.send(requests::Rmdir::new(path)).await?; + let status: responses::Status = self.send(requests::Rmdir::new(path)?).await?; status.into() } pub async fn realpath<'a, P>(&self, path: P) -> Result where - P: Into>, + P: ToByteStr<'a>, { - let mut name: responses::Name = self.send(requests::Realpath::new(path)).await?; + let mut name: responses::Name = self.send(requests::Realpath::new(path)?).await?; if name.items.is_empty() { Err(Error::custom("realpath returned no names")) } else { @@ -128,26 +131,26 @@ impl Operator { pub async fn stat<'a, P>(&self, path: P) -> Result where - P: Into>, + P: ToByteStr<'a>, { - let attrs: responses::Attrs = self.send(requests::Stat::new(path)).await?; + let attrs: responses::Attrs = self.send(requests::Stat::new(path)?).await?; Ok(attrs.attrs) } pub async fn rename<'a, F, T>(&self, from: F, to: T) -> Result<(), Error> where - F: Into>, - T: Into>, + F: ToByteStr<'a>, + T: ToByteStr<'a>, { - let status: responses::Status = self.send(requests::Rename::new(from, to)).await?; + let status: responses::Status = self.send(requests::Rename::new(from, to)?).await?; status.into() } pub async fn readlink<'a, P>(&self, path: P) -> Result where - P: Into>, + P: ToByteStr<'a>, { - let mut name: responses::Name = self.send(requests::Readlink::new(path)).await?; + let mut name: responses::Name = self.send(requests::Readlink::new(path)?).await?; if name.items.is_empty() { Err(Error::custom("readlink returned no names")) } else { @@ -157,10 +160,10 @@ impl Operator { pub async fn symlink<'a, L, O>(&self, original: O, link: L) -> Result<(), Error> where - O: Into>, - L: Into>, + O: ToByteStr<'a>, + L: ToByteStr<'a>, { - let status: responses::Status = self.send(requests::Symlink::new(original, link)).await?; + let status: responses::Status = self.send(requests::Symlink::new(original, link)?).await?; status.into() } @@ -175,14 +178,14 @@ impl Operator { pub async fn hardlink<'a, O, L>(&self, original: O, link: L) -> Result<(), Error> where - O: Into>, - L: Into>, + O: ToByteStr<'a>, + L: ToByteStr<'a>, { if self.extensions.lock().get("hardlink@openssh.com").is_none_or(|s| s != "1") { return Err(Error::Unsupported); } - let data = requests::ExtendedHardlink::new(original, link); + let data = requests::ExtendedHardlink::new(original, link)?; let status: responses::Status = self.send(requests::Extended::new("hardlink@openssh.com", data)).await?; status.into() diff --git a/yazi-sftp/src/requests/extended.rs b/yazi-sftp/src/requests/extended.rs index a10601dd..93943481 100644 --- a/yazi-sftp/src/requests/extended.rs +++ b/yazi-sftp/src/requests/extended.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, fmt::Debug}; use serde::{Deserialize, Serialize}; -use crate::ByteStr; +use crate::{ByteStr, Error, ToByteStr}; #[derive(Debug, Deserialize, Serialize)] pub struct Extended<'a, D> { @@ -49,12 +49,12 @@ pub struct ExtendedHardlink<'a> { } impl<'a> ExtendedHardlink<'a> { - pub fn new(original: O, link: L) -> Self + pub fn new(original: O, link: L) -> Result where - O: Into>, - L: Into>, + O: ToByteStr<'a>, + L: ToByteStr<'a>, { - Self { original: original.into(), link: link.into() } + Ok(Self { original: original.to_byte_str()?, link: link.to_byte_str()? }) } } diff --git a/yazi-sftp/src/requests/lstat.rs b/yazi-sftp/src/requests/lstat.rs index 4e1cb7be..55f87891 100644 --- a/yazi-sftp/src/requests/lstat.rs +++ b/yazi-sftp/src/requests/lstat.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::ByteStr; +use crate::{ByteStr, Error, ToByteStr}; #[derive(Debug, Deserialize, Serialize)] pub struct Lstat<'a> { @@ -9,7 +9,12 @@ pub struct Lstat<'a> { } impl Lstat<'_> { - pub fn new<'a>(path: impl Into>) -> Lstat<'a> { Lstat { id: 0, path: path.into() } } + pub fn new<'a, P>(path: P) -> Result, Error> + where + P: ToByteStr<'a>, + { + Ok(Lstat { id: 0, path: path.to_byte_str()? }) + } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } } diff --git a/yazi-sftp/src/requests/mkdir.rs b/yazi-sftp/src/requests/mkdir.rs index 05310eb8..4d37563e 100644 --- a/yazi-sftp/src/requests/mkdir.rs +++ b/yazi-sftp/src/requests/mkdir.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{ByteStr, fs::Attrs}; +use crate::{ByteStr, Error, ToByteStr, fs::Attrs}; #[derive(Debug, Deserialize, Serialize)] pub struct Mkdir<'a> { @@ -10,11 +10,11 @@ pub struct Mkdir<'a> { } impl<'a> Mkdir<'a> { - pub fn new

(path: P, attrs: Attrs) -> Self + pub fn new

(path: P, attrs: Attrs) -> Result where - P: Into>, + P: ToByteStr<'a>, { - Self { id: 0, path: path.into(), attrs } + Ok(Self { id: 0, path: path.to_byte_str()?, attrs }) } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() + self.attrs.len() } diff --git a/yazi-sftp/src/requests/open.rs b/yazi-sftp/src/requests/open.rs index 48356fa9..22f22861 100644 --- a/yazi-sftp/src/requests/open.rs +++ b/yazi-sftp/src/requests/open.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use serde::{Deserialize, Serialize}; -use crate::{ByteStr, fs::{Attrs, Flags}}; +use crate::{ByteStr, Error, ToByteStr, fs::{Attrs, Flags}}; #[derive(Debug, Deserialize, Serialize)] pub struct Open<'a> { @@ -13,11 +13,11 @@ pub struct Open<'a> { } impl<'a> Open<'a> { - pub fn new

(path: P, flags: Flags, attrs: &'a Attrs) -> Self + pub fn new

(path: P, flags: Flags, attrs: &'a Attrs) -> Result where - P: Into>, + P: ToByteStr<'a>, { - Self { id: 0, path: path.into(), flags, attrs: Cow::Borrowed(attrs) } + Ok(Self { id: 0, path: path.to_byte_str()?, flags, attrs: Cow::Borrowed(attrs) }) } pub fn len(&self) -> usize { diff --git a/yazi-sftp/src/requests/open_dir.rs b/yazi-sftp/src/requests/open_dir.rs index 5b0e3bbc..3d29b54f 100644 --- a/yazi-sftp/src/requests/open_dir.rs +++ b/yazi-sftp/src/requests/open_dir.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::ByteStr; +use crate::{ByteStr, Error, ToByteStr}; #[derive(Debug, Deserialize, Serialize)] pub struct OpenDir<'a> { @@ -9,7 +9,12 @@ pub struct OpenDir<'a> { } impl<'a> OpenDir<'a> { - pub fn new(path: impl Into>) -> Self { Self { id: 0, path: path.into() } } + pub fn new

(path: P) -> Result + where + P: ToByteStr<'a>, + { + Ok(Self { id: 0, path: path.to_byte_str()? }) + } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } } diff --git a/yazi-sftp/src/requests/readlink.rs b/yazi-sftp/src/requests/readlink.rs index aea62145..9f9ed259 100644 --- a/yazi-sftp/src/requests/readlink.rs +++ b/yazi-sftp/src/requests/readlink.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::ByteStr; +use crate::{ByteStr, Error, ToByteStr}; #[derive(Debug, Deserialize, Serialize)] pub struct Readlink<'a> { @@ -9,7 +9,12 @@ pub struct Readlink<'a> { } impl<'a> Readlink<'a> { - pub fn new(path: impl Into>) -> Self { Self { id: 0, path: path.into() } } + pub fn new

(path: P) -> Result + where + P: ToByteStr<'a>, + { + Ok(Self { id: 0, path: path.to_byte_str()? }) + } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } } diff --git a/yazi-sftp/src/requests/realpath.rs b/yazi-sftp/src/requests/realpath.rs index 00268921..bad53344 100644 --- a/yazi-sftp/src/requests/realpath.rs +++ b/yazi-sftp/src/requests/realpath.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::ByteStr; +use crate::{ByteStr, Error, ToByteStr}; #[derive(Debug, Deserialize, Serialize)] pub struct Realpath<'a> { @@ -9,7 +9,12 @@ pub struct Realpath<'a> { } impl<'a> Realpath<'a> { - pub fn new(path: impl Into>) -> Self { Self { id: 0, path: path.into() } } + pub fn new

(path: P) -> Result + where + P: ToByteStr<'a>, + { + Ok(Self { id: 0, path: path.to_byte_str()? }) + } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } } diff --git a/yazi-sftp/src/requests/remove.rs b/yazi-sftp/src/requests/remove.rs index 71a11381..db9ea2d5 100644 --- a/yazi-sftp/src/requests/remove.rs +++ b/yazi-sftp/src/requests/remove.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::ByteStr; +use crate::{ByteStr, Error, ToByteStr}; #[derive(Debug, Deserialize, Serialize)] pub struct Remove<'a> { @@ -9,7 +9,12 @@ pub struct Remove<'a> { } impl<'a> Remove<'a> { - pub fn new(path: impl Into>) -> Self { Self { id: 0, path: path.into() } } + pub fn new

(path: P) -> Result + where + P: ToByteStr<'a>, + { + Ok(Self { id: 0, path: path.to_byte_str()? }) + } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } } diff --git a/yazi-sftp/src/requests/rename.rs b/yazi-sftp/src/requests/rename.rs index 698ad86c..749495f0 100644 --- a/yazi-sftp/src/requests/rename.rs +++ b/yazi-sftp/src/requests/rename.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::ByteStr; +use crate::{ByteStr, Error, ToByteStr}; #[derive(Debug, Deserialize, Serialize)] pub struct Rename<'a> { @@ -10,12 +10,12 @@ pub struct Rename<'a> { } impl<'a> Rename<'a> { - pub fn new(from: F, to: T) -> Self + pub fn new(from: F, to: T) -> Result where - F: Into>, - T: Into>, + F: ToByteStr<'a>, + T: ToByteStr<'a>, { - Self { id: 0, from: from.into(), to: to.into() } + Ok(Self { id: 0, from: from.to_byte_str()?, to: to.to_byte_str()? }) } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.from.len() + 4 + self.to.len() } diff --git a/yazi-sftp/src/requests/rmdir.rs b/yazi-sftp/src/requests/rmdir.rs index 686afe69..3d9a26c6 100644 --- a/yazi-sftp/src/requests/rmdir.rs +++ b/yazi-sftp/src/requests/rmdir.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::ByteStr; +use crate::{ByteStr, Error, ToByteStr}; #[derive(Debug, Deserialize, Serialize)] pub struct Rmdir<'a> { @@ -9,7 +9,12 @@ pub struct Rmdir<'a> { } impl<'a> Rmdir<'a> { - pub fn new(path: impl Into>) -> Self { Self { id: 0, path: path.into() } } + pub fn new

(path: P) -> Result + where + P: ToByteStr<'a>, + { + Ok(Self { id: 0, path: path.to_byte_str()? }) + } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } } diff --git a/yazi-sftp/src/requests/set_stat.rs b/yazi-sftp/src/requests/set_stat.rs index 073350cc..4e618489 100644 --- a/yazi-sftp/src/requests/set_stat.rs +++ b/yazi-sftp/src/requests/set_stat.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use serde::{Deserialize, Serialize}; -use crate::{ByteStr, fs::Attrs}; +use crate::{ByteStr, Error, ToByteStr, fs::Attrs}; #[derive(Debug, Deserialize, Serialize)] pub struct SetStat<'a> { @@ -12,8 +12,11 @@ pub struct SetStat<'a> { } impl<'a> SetStat<'a> { - pub fn new(path: impl Into>, attrs: Attrs) -> Self { - Self { id: 0, path: path.into(), attrs } + pub fn new

(path: P, attrs: Attrs) -> Result + where + P: ToByteStr<'a>, + { + Ok(Self { id: 0, path: path.to_byte_str()?, attrs }) } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() + self.attrs.len() } diff --git a/yazi-sftp/src/requests/stat.rs b/yazi-sftp/src/requests/stat.rs index 51209c46..58addd47 100644 --- a/yazi-sftp/src/requests/stat.rs +++ b/yazi-sftp/src/requests/stat.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::ByteStr; +use crate::{ByteStr, Error, ToByteStr}; #[derive(Debug, Deserialize, Serialize)] pub struct Stat<'a> { @@ -9,7 +9,12 @@ pub struct Stat<'a> { } impl<'a> Stat<'a> { - pub fn new(path: impl Into>) -> Self { Self { id: 0, path: path.into() } } + pub fn new

(path: P) -> Result + where + P: ToByteStr<'a>, + { + Ok(Self { id: 0, path: path.to_byte_str()? }) + } pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } } diff --git a/yazi-sftp/src/requests/symlink.rs b/yazi-sftp/src/requests/symlink.rs index 11b010ad..4f88ce2a 100644 --- a/yazi-sftp/src/requests/symlink.rs +++ b/yazi-sftp/src/requests/symlink.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::ByteStr; +use crate::{ByteStr, Error, ToByteStr}; #[derive(Debug, Deserialize, Serialize)] pub struct Symlink<'a> { @@ -10,12 +10,12 @@ pub struct Symlink<'a> { } impl<'a> Symlink<'a> { - pub fn new(link: L, original: O) -> Self + pub fn new(link: L, original: O) -> Result where - L: Into>, - O: Into>, + L: ToByteStr<'a>, + O: ToByteStr<'a>, { - Self { id: 0, link: link.into(), original: original.into() } + Ok(Self { id: 0, link: link.to_byte_str()?, original: original.to_byte_str()? }) } pub fn len(&self) -> usize { diff --git a/yazi-sftp/src/wtf.rs b/yazi-sftp/src/wtf.rs new file mode 100644 index 00000000..bc097032 --- /dev/null +++ b/yazi-sftp/src/wtf.rs @@ -0,0 +1,87 @@ +use std::{borrow::Cow, ffi::{OsStr, OsString}, os::windows::ffi::{OsStrExt, OsStringExt}}; + +pub(super) fn bytes_to_wide(mut bytes: &[u8]) -> Cow<'_, OsStr> { + let mut wide: Option> = None; + while !bytes.is_empty() { + match (str::from_utf8(bytes), &mut wide) { + (Ok(valid), None) => { + return OsStr::new(valid).into(); + } + (Ok(valid), Some(wide)) => { + for ch in valid.chars() { + wide.extend(ch.encode_utf16(&mut [0u16; 2]).iter()); + } + break; + } + (Err(err), _) => { + let wide = wide.get_or_insert_with(|| Vec::with_capacity(bytes.len())); + + let valid = unsafe { str::from_utf8_unchecked(&bytes[..err.valid_up_to()]) }; + for c in valid.chars() { + wide.extend(c.encode_utf16(&mut [0u16; 2]).iter()); + } + bytes = &bytes[valid.len()..]; + + let invalid = err.error_len().unwrap_or(bytes.len()); + for &b in &bytes[..invalid] { + wide.push(0xdc00 + b as u16); + } + bytes = &bytes[invalid..]; + } + } + } + OsString::from_wide(&wide.unwrap_or_default()).into() +} + +pub(super) fn wide_to_bytes(wide: &OsStr) -> Option> { + if let Some(s) = wide.to_str() { + return Some(s.as_bytes().into()); + } + + let mut it = wide.encode_wide(); + let mut out = Vec::with_capacity(wide.len()); + + while let Some(w) = it.next() { + if (0xdc00..=0xdcff).contains(&w) { + out.push((w - 0xdc00) as u8); + } else if (0xd800..=0xdbff).contains(&w) { + let x = it.next().filter(|x| (0xdc00..=0xdfff).contains(x))?; + let c = char::from_u32(0x10000 + (((w as u32 - 0xd800) << 10) | (x as u32 - 0xdc00)))?; + out.extend_from_slice(c.encode_utf8(&mut [0u8; 4]).as_bytes()); + } else if (0xdc00..=0xdfff).contains(&w) { + return None; + } else { + let c = char::from_u32(w as u32)?; + out.extend_from_slice(c.encode_utf8(&mut [0u8; 4]).as_bytes()); + } + } + + Some(out.into()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn wtf8_roundtrip() { + let b = &[ + b'\0', // NUL + 0xff, // 0xFF + b'a', b'b', b'c', // abc + 0xf0, 0x9f, 0x98, 0x80, // 😀 + 0xc3, 0x28, // illegal UTF-8 + ]; + assert_eq!(&*wide_to_bytes(&bytes_to_wide(b)).unwrap(), b); + } + + #[test] + #[cfg(windows)] + fn low_surrogates_for_non_utf8() { + use std::os::windows::ffi::OsStrExt; + + let os = bytes_to_wide(b"\xFF"); + let wide: Vec = os.encode_wide().collect(); + assert_eq!(wide, vec![0xdc00 + 0xff]); + } +} diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index 1d9243bb..61d8c882 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -21,7 +21,7 @@ hashbrown = { workspace = true } memchr = "2.7.6" ordered-float = { workspace = true } parking_lot = { workspace = true } -percent-encoding = "2.3.2" +percent-encoding = { workspace = true } serde = { workspace = true } tokio = { workspace = true } diff --git a/yazi-shared/src/loc/loc.rs b/yazi-shared/src/loc/loc.rs index b50db65c..9fbd51b1 100644 --- a/yazi-shared/src/loc/loc.rs +++ b/yazi-shared/src/loc/loc.rs @@ -11,6 +11,10 @@ pub struct Loc<'a> { pub(super) urn: usize, } +impl Default for Loc<'_> { + fn default() -> Self { Self { inner: Path::new(""), uri: 0, urn: 0 } } +} + impl Deref for Loc<'_> { type Target = Path; diff --git a/yazi-shared/src/scheme/scheme.rs b/yazi-shared/src/scheme/scheme.rs index cd010873..03fccada 100644 --- a/yazi-shared/src/scheme/scheme.rs +++ b/yazi-shared/src/scheme/scheme.rs @@ -18,6 +18,10 @@ impl Hash for Scheme { fn hash(&self, state: &mut H) { self.as_ref().hash(state); } } +impl PartialEq> for Scheme { + fn eq(&self, other: &SchemeRef<'_>) -> bool { self.as_ref() == *other } +} + impl Scheme { #[inline] pub fn as_ref(&self) -> SchemeRef<'_> { self.into() } diff --git a/yazi-shared/src/string.rs b/yazi-shared/src/string.rs index 3ef681ff..20f961a5 100644 --- a/yazi-shared/src/string.rs +++ b/yazi-shared/src/string.rs @@ -33,6 +33,5 @@ impl IntoStringLossy for Cow<'_, OsStr> { } impl IntoStringLossy for &UrlBuf { - // FIXME: remove fn into_string_lossy(self) -> String { self.os_str().into_string_lossy() } } diff --git a/yazi-shared/src/url/buf.rs b/yazi-shared/src/url/buf.rs index 81cdc29d..0b13341c 100644 --- a/yazi-shared/src/url/buf.rs +++ b/yazi-shared/src/url/buf.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, hash::BuildHasher, path::{Path, PathBuf}, str::FromStr, sync::OnceLock}; +use std::{borrow::Cow, ffi::OsStr, fmt::{Debug, Formatter}, path::{Path, PathBuf}, str::FromStr, sync::OnceLock}; use anyhow::Result; use serde::{Deserialize, Serialize}; @@ -101,9 +101,6 @@ impl UrlBuf { pub fn rebase(&self, base: &Path) -> Self { Self { loc: self.loc.rebase(base), scheme: self.scheme.clone() } } - - #[inline] - pub fn hash_u64(&self) -> u64 { foldhash::fast::FixedState::default().hash_one(self) } } impl UrlBuf { diff --git a/yazi-shared/src/url/component.rs b/yazi-shared/src/url/component.rs index f1de477b..4869194e 100644 --- a/yazi-shared/src/url/component.rs +++ b/yazi-shared/src/url/component.rs @@ -81,7 +81,7 @@ impl<'a> Components<'a> { return path.as_os_str().into(); } - let mut s = OsString::from(Encode::from(self.url).to_string()); + let mut s = OsString::from(Encode(self.url).to_string()); s.reserve_exact(path.as_os_str().len()); s.push(path); s.into() diff --git a/yazi-shared/src/url/cow.rs b/yazi-shared/src/url/cow.rs index e624da03..8474bde5 100644 --- a/yazi-shared/src/url/cow.rs +++ b/yazi-shared/src/url/cow.rs @@ -12,7 +12,7 @@ pub enum UrlCow<'a> { } impl Default for UrlCow<'_> { - fn default() -> Self { Self::Owned { loc: Default::default(), scheme: Default::default() } } + fn default() -> Self { Self::Borrowed { loc: Default::default(), scheme: Default::default() } } } impl<'a> From> for UrlCow<'a> { @@ -20,9 +20,7 @@ impl<'a> From> for UrlCow<'a> { } impl<'a> From<&'a UrlBuf> for UrlCow<'a> { - fn from(value: &'a UrlBuf) -> Self { - Self::Borrowed { loc: value.loc.as_loc(), scheme: SchemeCow::from(&value.scheme) } - } + fn from(value: &'a UrlBuf) -> Self { value.as_url().into() } } impl<'a> From<&'a UrlCow<'a>> for UrlCow<'a> { @@ -33,6 +31,10 @@ impl From for UrlCow<'_> { fn from(value: UrlBuf) -> Self { Self::Owned { loc: value.loc, scheme: value.scheme.into() } } } +impl From for UrlCow<'_> { + fn from(value: PathBuf) -> Self { UrlBuf::from(value).into() } +} + impl From> for UrlBuf { fn from(value: UrlCow<'_>) -> Self { value.into_owned() } } diff --git a/yazi-shared/src/url/display.rs b/yazi-shared/src/url/display.rs index 5935ab78..b9768530 100644 --- a/yazi-shared/src/url/display.rs +++ b/yazi-shared/src/url/display.rs @@ -12,7 +12,7 @@ impl<'a> std::fmt::Display for Display<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Url { loc, scheme } = self.inner; if scheme.is_virtual() { - Encode::from(self.inner).fmt(f)?; + Encode(self.inner).fmt(f)?; } loc.display().fmt(f) } diff --git a/yazi-shared/src/url/encode.rs b/yazi-shared/src/url/encode.rs index 0a40c604..86fbfeb7 100644 --- a/yazi-shared/src/url/encode.rs +++ b/yazi-shared/src/url/encode.rs @@ -5,11 +5,7 @@ use percent_encoding::{AsciiSet, CONTROLS, PercentEncode, percent_encode}; use crate::{scheme::SchemeRef, url::{AsUrl, Url, UrlBuf}}; #[derive(Clone, Copy)] -pub struct Encode<'a>(Url<'a>); - -impl<'a> From> for Encode<'a> { - fn from(value: Url<'a>) -> Self { Self(value) } -} +pub struct Encode<'a>(pub Url<'a>); impl<'a> From<&'a UrlBuf> for Encode<'a> { fn from(value: &'a UrlBuf) -> Self { Self(value.as_url()) } @@ -69,11 +65,7 @@ impl Display for Encode<'_> { // --- Tilded #[derive(Clone, Copy)] -pub struct EncodeTilded<'a>(Url<'a>); - -impl<'a> From> for EncodeTilded<'a> { - fn from(value: Url<'a>) -> Self { Self(value) } -} +pub struct EncodeTilded<'a>(pub Url<'a>); impl<'a> From<&'a UrlBuf> for EncodeTilded<'a> { fn from(value: &'a UrlBuf) -> Self { Self(value.as_url()) } diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 591c2161..eaade3f6 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -26,7 +26,7 @@ impl Debug for Url<'_> { if self.scheme == SchemeRef::Regular { write!(f, "{}", self.loc.display()) } else { - write!(f, "{}{}", Encode::from(*self), self.loc.display()) + write!(f, "{}{}", Encode(*self), self.loc.display()) } } } @@ -49,7 +49,14 @@ impl<'a> Url<'a> { pub fn is_search(self) -> bool { matches!(self.scheme, SchemeRef::Search(_)) } #[inline] - pub fn is_absolute(self) -> bool { self.loc.is_absolute() } + pub fn is_absolute(self) -> bool { + use SchemeRef as S; + + match self.scheme { + S::Regular | S::Search(_) => self.loc.is_absolute(), + S::Archive(_) | S::Sftp(_) => self.loc.has_root(), + } + } #[inline] pub fn has_root(self) -> bool { self.loc.has_root() } diff --git a/yazi-shim/Cargo.toml b/yazi-shim/Cargo.toml new file mode 100644 index 00000000..069ae5c2 --- /dev/null +++ b/yazi-shim/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "yazi-shim" +version = "25.9.15" +edition = "2024" +license = "MIT" +authors = [ "sxyazi " ] +description = "Yazi crate shims" +homepage = "https://yazi-rs.github.io" +repository = "https://github.com/sxyazi/yazi" + +[dependencies] +yazi-macro = { path = "../yazi-macro", version = "25.9.15" } + +# External dependencies +twox-hash = { workspace = true } diff --git a/yazi-shim/src/lib.rs b/yazi-shim/src/lib.rs new file mode 100644 index 00000000..856012c3 --- /dev/null +++ b/yazi-shim/src/lib.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(twox); diff --git a/yazi-plugin/src/twox.rs b/yazi-shim/src/twox.rs similarity index 87% rename from yazi-plugin/src/twox.rs rename to yazi-shim/src/twox.rs index 5da847fa..73eb821c 100644 --- a/yazi-plugin/src/twox.rs +++ b/yazi-shim/src/twox.rs @@ -13,5 +13,5 @@ impl Twox128 { impl Hasher for Twox128 { fn write(&mut self, bytes: &[u8]) { self.0.write(bytes) } - fn finish(&self) -> u64 { unreachable!() } + fn finish(&self) -> u64 { unimplemented!() } } diff --git a/yazi-vfs/src/fns.rs b/yazi-vfs/src/fns.rs index 40c518d2..39a4512b 100644 --- a/yazi-vfs/src/fns.rs +++ b/yazi-vfs/src/fns.rs @@ -3,7 +3,7 @@ use std::{ffi::OsString, io}; use tokio::{select, sync::{mpsc, oneshot}}; use yazi_fs::cha::Cha; use yazi_macro::ok_or_not_found; -use yazi_shared::url::{AsUrl, UrlBuf, UrlLike}; +use yazi_shared::url::{AsUrl, Url, UrlBuf, UrlLike}; use crate::provider; @@ -63,23 +63,27 @@ async fn _unique_name(mut url: UrlBuf, append: bool) -> io::Result { Ok(url) } -pub fn copy_with_progress( - from: &UrlBuf, - to: &UrlBuf, - cha: Cha, -) -> mpsc::Receiver> { +pub fn copy_with_progress(from: U, to: V, cha: Cha) -> mpsc::Receiver> +where + U: AsUrl, + V: AsUrl, +{ + _copy_with_progress(from.as_url(), to.as_url(), cha) +} + +fn _copy_with_progress(from: Url, to: Url, cha: Cha) -> mpsc::Receiver> { let (prog_tx, prog_rx) = mpsc::channel(1); let (done_tx, mut done_rx) = oneshot::channel(); tokio::spawn({ - let (from, to) = (from.clone(), to.clone()); + let (from, to) = (from.to_owned(), to.to_owned()); async move { - done_tx.send(provider::copy(&from, &to, cha).await).ok(); + done_tx.send(provider::copy(from, to, cha).await).ok(); } }); tokio::spawn({ - let (prog_tx, to) = (prog_tx.clone(), to.clone()); + let (prog_tx, to) = (prog_tx.to_owned(), to.to_owned()); async move { let mut last = 0; let mut done = None; diff --git a/yazi-watcher/Cargo.toml b/yazi-watcher/Cargo.toml index 6c04486b..e8d086da 100644 --- a/yazi-watcher/Cargo.toml +++ b/yazi-watcher/Cargo.toml @@ -20,9 +20,12 @@ yazi-vfs = { path = "../yazi-vfs", version = "25.9.15" } tracing = { workspace = true } # External dependencies -anyhow = { workspace = true } -hashbrown = { workspace = true } -notify = { version = "8.2.0", default-features = false, features = [ "macos_fsevent" ] } -parking_lot = { workspace = true } -tokio = { workspace = true } -tokio-stream = { workspace = true } +anyhow = { workspace = true } +base64 = { workspace = true } +hashbrown = { workspace = true } +notify = { version = "8.2.0", default-features = false, features = [ "macos_fsevent" ] } +parking_lot = { workspace = true } +percent-encoding = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } +twox-hash = { workspace = true } diff --git a/yazi-watcher/src/backend.rs b/yazi-watcher/src/backend.rs new file mode 100644 index 00000000..1212de9e --- /dev/null +++ b/yazi-watcher/src/backend.rs @@ -0,0 +1,57 @@ +use anyhow::Result; +use tokio::sync::mpsc; +use yazi_shared::url::AsUrl; + +use crate::{Reporter, WATCHED, local::{self, LINKED, Linked}, remote}; + +pub(crate) struct Backend { + local: local::Local, + remote: remote::Remote, + pub(super) reporter: Reporter, +} + +impl Backend { + pub(crate) fn serve() -> Self { + #[cfg(any(target_os = "linux", target_os = "macos"))] + yazi_fs::mounts::Partitions::monitor(&yazi_fs::mounts::PARTITIONS, || { + yazi_macro::err!(yazi_dds::Pubsub::pub_after_mount()) + }); + + let (local_tx, local_rx) = mpsc::unbounded_channel(); + let (remote_tx, remote_rx) = mpsc::unbounded_channel(); + let reporter = Reporter { local_tx, remote_tx }; + + Self { + local: local::Local::serve(local_rx, reporter.clone()), + remote: remote::Remote::serve(remote_rx, reporter.clone()), + reporter, + } + } + + pub(super) fn watch(&mut self, url: impl AsUrl) -> Result<()> { + let url = url.as_url(); + if let Some(path) = url.as_path() { + self.local.watch(path)?; + } else { + self.remote.watch(url)?; + } + + Ok(()) + } + + pub(super) fn unwatch(&mut self, url: impl AsUrl) -> Result<()> { + let url = url.as_url(); + if let Some(path) = url.as_path() { + self.local.unwatch(path)?; + } else { + self.remote.unwatch(url)?; + } + + Ok(()) + } + + pub(super) async fn sync(self) -> Self { + Linked::sync(&LINKED, &WATCHED).await; + self + } +} diff --git a/yazi-watcher/src/backend/backend.rs b/yazi-watcher/src/backend/backend.rs deleted file mode 100644 index 6b3e45a6..00000000 --- a/yazi-watcher/src/backend/backend.rs +++ /dev/null @@ -1,74 +0,0 @@ -use anyhow::Result; -use hashbrown::HashSet; -use tokio::sync::mpsc; -use tracing::error; -use yazi_shared::url::{AsUrl, Url, UrlBuf, UrlLike}; - -use crate::{LINKED, WATCHED, backend}; - -pub(crate) struct Backend { - local: backend::Local, -} - -impl Backend { - pub(crate) fn serve(out_tx: mpsc::UnboundedSender) -> Self { - #[cfg(any(target_os = "linux", target_os = "macos"))] - yazi_fs::mounts::Partitions::monitor(&yazi_fs::mounts::PARTITIONS, || { - yazi_macro::err!(yazi_dds::Pubsub::pub_after_mount()) - }); - - Self { local: backend::Local::serve(out_tx) } - } - - pub(crate) async fn sync(mut self, to_unwatch: Vec, to_watch: Vec) -> Self { - if to_unwatch.is_empty() && to_watch.is_empty() { - return self; - } - - tokio::task::spawn_blocking(move || { - for u in to_unwatch { - match self.unwatch(&u) { - Ok(()) => WATCHED.write().remove(&u), - Err(e) => error!("Unwatch failed: {e:?}"), - } - } - for u in to_watch { - match self.watch(&u) { - Ok(()) => WATCHED.write().insert(u), - Err(e) => error!("Watch failed: {e:?}"), - } - } - self - }) - .await - .unwrap() - } - - pub(crate) fn push_files(out_tx: &mpsc::UnboundedSender, urls: I) - where - I: IntoIterator, - T: Into, - { - let (mut todo, watched) = (HashSet::new(), WATCHED.read()); - for url in urls.into_iter().map(Into::into) { - let Some(parent) = url.parent() else { continue }; - if todo.contains(&parent) { - todo.insert(url); - } else if watched.contains(parent) - || LINKED.read().from_dir(parent).any(|p| watched.contains(Url::regular(p))) - { - todo.insert(parent.to_owned()); - todo.insert(url); - } - } - todo.into_iter().for_each(|u| _ = out_tx.send(u)); - } - - fn watch(&mut self, url: impl AsUrl) -> Result<()> { - if let Some(path) = url.as_url().as_path() { self.local.watch(path) } else { Ok(()) } - } - - fn unwatch(&mut self, url: impl AsUrl) -> Result<()> { - if let Some(path) = url.as_url().as_path() { self.local.unwatch(path) } else { Ok(()) } - } -} diff --git a/yazi-watcher/src/backend/local.rs b/yazi-watcher/src/backend/local.rs deleted file mode 100644 index 621ae714..00000000 --- a/yazi-watcher/src/backend/local.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::{path::Path, time::Duration}; - -use anyhow::Result; -use notify::{ErrorKind::WatchNotFound, PollWatcher, RecommendedWatcher, RecursiveMode, Watcher}; -use tokio::sync::mpsc; -use yazi_shared::url::UrlBuf; - -use crate::backend::Backend; - -pub(super) struct Local(Box); - -impl Local { - pub(super) fn serve(out_tx: mpsc::UnboundedSender) -> Self { - let handler = move |res: Result| { - let Ok(event) = res else { return }; - if event.kind.is_access() { - return; - } - Backend::push_files(&out_tx, event.paths); - }; - - let config = notify::Config::default().with_poll_interval(Duration::from_millis(500)); - Self(if yazi_adapter::WSL.get() || cfg!(target_os = "netbsd") { - Box::new(PollWatcher::new(handler, config).unwrap()) - } else { - Box::new(RecommendedWatcher::new(handler, config).unwrap()) - }) - } - - pub(super) fn watch(&mut self, path: &Path) -> Result<()> { - Ok(self.0.watch(path, RecursiveMode::NonRecursive)?) - } - - pub(super) fn unwatch(&mut self, path: &Path) -> Result<()> { - match self.0.unwatch(path) { - Ok(()) => Ok(()), - Err(e) if matches!(e.kind, WatchNotFound) => Ok(()), - Err(e) => Err(e)?, - } - } -} diff --git a/yazi-watcher/src/backend/mod.rs b/yazi-watcher/src/backend/mod.rs deleted file mode 100644 index e0f9955f..00000000 --- a/yazi-watcher/src/backend/mod.rs +++ /dev/null @@ -1 +0,0 @@ -yazi_macro::mod_flat!(backend local); diff --git a/yazi-watcher/src/lib.rs b/yazi-watcher/src/lib.rs index 755c2954..614d0274 100644 --- a/yazi-watcher/src/lib.rs +++ b/yazi-watcher/src/lib.rs @@ -1,13 +1,13 @@ -yazi_macro::mod_pub!(backend); +yazi_macro::mod_pub!(local remote); -yazi_macro::mod_flat!(linked watched watcher); +yazi_macro::mod_flat!(backend reporter watched watcher); -pub static LINKED: yazi_shared::RoCell> = yazi_shared::RoCell::new(); pub static WATCHED: yazi_shared::RoCell> = yazi_shared::RoCell::new(); pub static WATCHER: yazi_shared::RoCell = yazi_shared::RoCell::new(); pub fn init() { - LINKED.with(<_>::default); WATCHED.with(<_>::default); WATCHER.init(tokio::sync::Semaphore::new(1)); + + local::init(); } diff --git a/yazi-watcher/src/linked.rs b/yazi-watcher/src/local/linked.rs similarity index 68% rename from yazi-watcher/src/linked.rs rename to yazi-watcher/src/local/linked.rs index 819b7106..db2944f2 100644 --- a/yazi-watcher/src/linked.rs +++ b/yazi-watcher/src/local/linked.rs @@ -1,6 +1,6 @@ use std::{iter, ops::{Deref, DerefMut}, path::{Path, PathBuf}}; -use hashbrown::{HashMap, HashSet}; +use hashbrown::HashMap; use parking_lot::RwLock; use yazi_shared::url::Url; @@ -46,25 +46,22 @@ impl Linked { } } - pub(super) async fn sync(linked: &'static RwLock, watched: &'static RwLock) { + pub(crate) async fn sync(linked: &'static RwLock, watched: &'static RwLock) { tokio::task::spawn_blocking(move || { - let mut new: HashSet<_> = watched.read().paths().map(ToOwned::to_owned).collect(); - let mut linked = linked.write(); + let watched = watched.read(); - linked.retain(|k, _| new.remove(k)); - for from in new { - linked.insert(from, PathBuf::new()); - } + // Remove entries that are no longer watched + linked.write().retain(|from, _| watched.contains(Url::regular(from))); - for (from, to) in linked.iter_mut() { + // Update existing entries and remove broken links + for from in watched.paths() { match std::fs::canonicalize(from) { - Ok(c) if c != *from && watched.read().contains(Url::regular(from)) => *to = c, - Ok(_) => *to = PathBuf::new(), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => *to = PathBuf::new(), + Ok(to) if to != *from => _ = linked.write().entry_ref(from).insert(to), + Ok(_) => _ = linked.write().remove(from), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => _ = linked.write().remove(from), Err(_) => {} } } - linked.retain(|_, v| !v.as_os_str().is_empty()); }) .await .ok(); diff --git a/yazi-watcher/src/local/local.rs b/yazi-watcher/src/local/local.rs new file mode 100644 index 00000000..fe263109 --- /dev/null +++ b/yazi-watcher/src/local/local.rs @@ -0,0 +1,79 @@ +use std::{path::Path, time::Duration}; + +use anyhow::Result; +use hashbrown::HashSet; +use notify::{ErrorKind::WatchNotFound, PollWatcher, RecommendedWatcher, RecursiveMode, Watcher}; +use tokio::{pin, sync::mpsc::{self, UnboundedReceiver}}; +use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; +use yazi_fs::{File, FilesOp, provider}; +use yazi_shared::url::{UrlBuf, UrlLike}; +use yazi_vfs::VfsFile; + +use crate::{Reporter, WATCHER}; + +pub(crate) struct Local(Box); + +impl Local { + pub(crate) fn serve(rx: mpsc::UnboundedReceiver, reporter: Reporter) -> Self { + tokio::spawn(Self::changed(rx)); + + let config = notify::Config::default().with_poll_interval(Duration::from_millis(500)); + let handler = move |res: Result| { + if let Ok(event) = res + && !event.kind.is_access() + { + reporter.report(event.paths); + } + }; + + Self(if yazi_adapter::WSL.get() || cfg!(target_os = "netbsd") { + Box::new(PollWatcher::new(handler, config).unwrap()) + } else { + Box::new(RecommendedWatcher::new(handler, config).unwrap()) + }) + } + + pub(crate) fn watch(&mut self, path: &Path) -> Result<()> { + Ok(self.0.watch(path, RecursiveMode::NonRecursive)?) + } + + pub(crate) fn unwatch(&mut self, path: &Path) -> Result<()> { + match self.0.unwatch(path) { + Ok(()) => Ok(()), + Err(e) if matches!(e.kind, WatchNotFound) => Ok(()), + Err(e) => Err(e)?, + } + } + + async fn changed(rx: UnboundedReceiver) { + // TODO: revert this once a new notification is implemented + let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(250)); + pin!(rx); + + while let Some(chunk) = rx.next().await { + let urls: HashSet<_> = chunk.into_iter().collect(); + + let _permit = WATCHER.acquire().await.unwrap(); + let mut ops = Vec::with_capacity(urls.len()); + + for u in urls { + let Some((parent, urn)) = u.pair() else { continue }; + let Ok(file) = File::new(&u).await else { + ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); + continue; + }; + + if let Some(p) = file.url.as_path() + && !provider::local::must_case_match(p).await + { + ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); + continue; + } + + ops.push(FilesOp::Upserting(parent.into(), [(urn.into(), file)].into())); + } + + FilesOp::mutate(ops); + } + } +} diff --git a/yazi-watcher/src/local/mod.rs b/yazi-watcher/src/local/mod.rs new file mode 100644 index 00000000..c902d310 --- /dev/null +++ b/yazi-watcher/src/local/mod.rs @@ -0,0 +1,5 @@ +yazi_macro::mod_flat!(linked local); + +pub static LINKED: yazi_shared::RoCell> = yazi_shared::RoCell::new(); + +pub(super) fn init() { LINKED.with(<_>::default); } diff --git a/yazi-watcher/src/remote/mod.rs b/yazi-watcher/src/remote/mod.rs new file mode 100644 index 00000000..362fa369 --- /dev/null +++ b/yazi-watcher/src/remote/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(remote); diff --git a/yazi-watcher/src/remote/remote.rs b/yazi-watcher/src/remote/remote.rs new file mode 100644 index 00000000..2ad480f8 --- /dev/null +++ b/yazi-watcher/src/remote/remote.rs @@ -0,0 +1,40 @@ +use std::time::Duration; + +use anyhow::Result; +use hashbrown::HashSet; +use tokio::{pin, sync::mpsc::UnboundedReceiver}; +use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; +use yazi_shared::url::{Url, UrlBuf, UrlLike}; + +use crate::{Reporter, WATCHER}; + +pub(crate) struct Remote; + +impl Remote { + pub(crate) fn serve(rx: UnboundedReceiver, _reporter: Reporter) -> Self { + tokio::spawn(Self::changed(rx)); + + Self + } + + pub(crate) fn watch(&mut self, _url: Url) -> Result<()> { Ok(()) } + + pub(crate) fn unwatch(&mut self, _url: Url) -> Result<()> { Ok(()) } + + async fn changed(rx: UnboundedReceiver) { + let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(250)); + pin!(rx); + + while let Some(chunk) = rx.next().await { + let urls: HashSet<_> = chunk.into_iter().collect(); + let _permit = WATCHER.acquire().await.unwrap(); + + for u in urls { + let Some((parent, urn)) = u.pair() else { continue }; + + // FIXME + tracing::debug!("Remote changed: {}", u.display()); + } + } + } +} diff --git a/yazi-watcher/src/reporter.rs b/yazi-watcher/src/reporter.rs new file mode 100644 index 00000000..4fc8733c --- /dev/null +++ b/yazi-watcher/src/reporter.rs @@ -0,0 +1,57 @@ +use tokio::sync::mpsc; +use yazi_shared::{scheme::SchemeRef, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}}; + +use crate::{WATCHED, local::LINKED}; + +#[derive(Clone)] +pub(crate) struct Reporter { + pub(super) local_tx: mpsc::UnboundedSender, + pub(super) remote_tx: mpsc::UnboundedSender, +} + +impl Reporter { + pub(crate) fn report<'a, I>(&self, urls: I) + where + I: IntoIterator, + I::Item: Into>, + { + for url in urls.into_iter().map(Into::into) { + match url.as_url().scheme { + SchemeRef::Regular | SchemeRef::Search(_) => self.report_local(url), + SchemeRef::Archive(_) => {} + SchemeRef::Sftp(_) => self.report_remote(url), + } + } + } + + fn report_local<'a>(&self, url: UrlCow<'a>) { + let Some((parent, name)) = url.pair() else { return }; + + // FIXME: LINKED should return Url instead of Path + let linked = LINKED.read(); + let linked = linked.from_dir(parent).map(Url::regular); + + let watched = WATCHED.read(); + for parent in [parent].into_iter().chain(linked) { + // SFTP caches + if let Some(dir) = watched.find_by_cache(&parent.loc) { + self.remote_tx.send(dir.join(name)).ok(); + self.remote_tx.send(dir.to_owned()).ok(); + } + if watched.contains(parent) { + self.local_tx.send(url.to_owned()).ok(); + self.local_tx.send(parent.to_owned()).ok(); + } + } + } + + fn report_remote<'a>(&self, url: UrlCow<'a>) { + let Some(parent) = url.parent() else { return }; + if !WATCHED.read().contains(&url) { + return; + } + + self.remote_tx.send(parent.to_owned()).ok(); + self.remote_tx.send(url.into_owned()).ok(); + } +} diff --git a/yazi-watcher/src/watched.rs b/yazi-watcher/src/watched.rs index c5a9f7af..3823cd73 100644 --- a/yazi-watcher/src/watched.rs +++ b/yazi-watcher/src/watched.rs @@ -1,7 +1,9 @@ use std::path::Path; use hashbrown::HashSet; -use yazi_shared::url::{AsUrl, UrlBuf, UrlLike}; +use percent_encoding::percent_decode_str; +use yazi_fs::{Xdg, path::PercentEncoding}; +use yazi_shared::{scheme::SchemeRef, url::{AsUrl, UrlBuf, UrlLike}}; #[derive(Default)] pub struct Watched(HashSet); @@ -25,4 +27,28 @@ impl Watched { #[inline] pub(crate) fn remove(&mut self, url: impl AsUrl) { self.0.remove(&url.as_url()); } + + pub(super) fn find_by_cache(&self, cache: &Path) -> Option<&UrlBuf> { + let mut it = cache.strip_prefix(Xdg::cache_dir()).ok()?.components(); + + let l1 = it.next()?.as_os_str().to_str()?; + let (l2, rel) = + if let Ok(p) = it.as_path().strip_prefix(".%2F") { (p, true) } else { (it.as_path(), false) }; + + let domain = percent_decode_str(l1.strip_prefix("sftp-")?).decode_utf8().ok()?; + let loc = l2.percent_decode(); + + self.0.iter().find(|u| { + if u.scheme != SchemeRef::Sftp(&domain) { + return false; + } + + let mut it = u.loc.components(); + if it.next() == Some(std::path::Component::RootDir) { + !rel && it.as_path().as_os_str().as_encoded_bytes() == loc.as_ref() + } else { + rel && u.loc.as_os_str().as_encoded_bytes() == loc.as_ref() + } + }) + } } diff --git a/yazi-watcher/src/watcher.rs b/yazi-watcher/src/watcher.rs index 7a0c0a20..de7fa1ee 100644 --- a/yazi-watcher/src/watcher.rs +++ b/yazi-watcher/src/watcher.rs @@ -1,45 +1,64 @@ -use std::time::Duration; - use hashbrown::HashSet; -use tokio::{pin, sync::{mpsc::{self, UnboundedReceiver}, watch}}; -use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; -use yazi_fs::{File, FilesOp, provider::local}; -use yazi_shared::url::{UrlBuf, UrlLike}; -use yazi_vfs::VfsFile; +use tokio::sync::watch; +use tracing::error; +use yazi_fs::FsUrl; +use yazi_shared::url::{UrlBuf, UrlCow, UrlLike}; -use crate::{LINKED, Linked, WATCHED, WATCHER, backend::Backend}; +use crate::{Reporter, WATCHED, backend::Backend}; pub struct Watcher { - in_tx: watch::Sender>, - out_tx: mpsc::UnboundedSender, + tx: watch::Sender>, + reporter: Reporter, } impl Watcher { pub fn serve() -> Self { - let (in_tx, in_rx) = watch::channel(Default::default()); - let (out_tx, out_rx) = mpsc::unbounded_channel(); + let (tx, rx) = watch::channel(Default::default()); - let backend = Backend::serve(out_tx.clone()); + let backend = Backend::serve(); + let reporter = backend.reporter.clone(); - tokio::spawn(Self::fan_in(in_rx, backend)); - tokio::spawn(Self::fan_out(out_rx)); - - Self { in_tx, out_tx } + tokio::spawn(Self::watched(rx, backend)); + Self { tx, reporter } } - pub fn watch<'a>(&mut self, it: impl Iterator) { - self.in_tx.send(it.cloned().collect()).ok(); + pub fn watch<'a, I>(&mut self, urls: I) + where + I: IntoIterator, + I::Item: Into>, + { + let it = urls.into_iter(); + let mut set = HashSet::with_capacity(it.size_hint().0); + + for url in it.map(Into::into) { + if !url.is_absolute() { + continue; + } else if let Some(cache) = url.cache() { + set.insert(cache.into()); + } + set.insert(url.into_owned()); + } + + self.tx.send(set).ok(); } - pub fn push_files(&self, urls: Vec) { Backend::push_files(&self.out_tx, urls); } + pub fn report<'a, I>(&self, urls: I) + where + I: IntoIterator, + I::Item: Into>, + { + self.reporter.report(urls); + } - async fn fan_in(mut rx: watch::Receiver>, mut backend: Backend) { + async fn watched(mut rx: watch::Receiver>, mut backend: Backend) { loop { let (to_unwatch, to_watch) = WATCHED.read().diff(&rx.borrow_and_update()); - backend = backend.sync(to_unwatch, to_watch).await; - if !rx.has_changed().unwrap_or(false) { - Linked::sync(&LINKED, &WATCHED).await; + if !to_unwatch.is_empty() || !to_watch.is_empty() { + backend = Self::sync(backend, to_unwatch, to_watch).await; + if !rx.has_changed().unwrap_or(false) { + backend = backend.sync().await; + } } if rx.changed().await.is_err() { @@ -48,35 +67,23 @@ impl Watcher { } } - async fn fan_out(rx: UnboundedReceiver) { - // TODO: revert this once a new notification is implemented - let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(250)); - pin!(rx); - - while let Some(chunk) = rx.next().await { - let urls: HashSet<_> = chunk.into_iter().collect(); - - let _permit = WATCHER.acquire().await.unwrap(); - let mut ops = Vec::with_capacity(urls.len()); - - for u in urls { - let Some((parent, urn)) = u.pair() else { continue }; - let Ok(file) = File::new(&u).await else { - ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); - continue; - }; - - if let Some(p) = file.url.as_path() - && !local::must_case_match(p).await - { - ops.push(FilesOp::Deleting(parent.into(), [urn.into()].into())); - continue; + async fn sync(mut backend: Backend, to_unwatch: Vec, to_watch: Vec) -> Backend { + tokio::task::spawn_blocking(move || { + for u in to_unwatch { + match backend.unwatch(&u) { + Ok(()) => WATCHED.write().remove(&u), + Err(e) => error!("Unwatch failed: {e:?}"), } - - ops.push(FilesOp::Upserting(parent.into(), [(urn.into(), file)].into())); } - - FilesOp::mutate(ops); - } + for u in to_watch { + match backend.watch(&u) { + Ok(()) => WATCHED.write().insert(u), + Err(e) => error!("Watch failed: {e:?}"), + } + } + backend + }) + .await + .unwrap() } }