mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
feat: encode SFTP paths on Windows in a revised WTF-8 (#3238)
This commit is contained in:
parent
554cb52cc5
commit
c68e2df8c0
81 changed files with 1393 additions and 459 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ impl Actor for UpdateSucceed {
|
|||
const NAME: &str = "update_succeed";
|
||||
|
||||
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
|
||||
cx.mgr.watcher.push_files(opt.urls);
|
||||
cx.mgr.watcher.report(opt.urls);
|
||||
succ!();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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<Cwd> = RoCell::new();
|
||||
|
||||
|
|
@ -46,6 +46,35 @@ impl Cwd {
|
|||
true
|
||||
}
|
||||
|
||||
pub fn ensure(url: Url) -> Cow<Path> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
47
yazi-fs/src/hash.rs
Normal file
47
yazi-fs/src/hash.rs
Normal file
|
|
@ -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<T: AsUrl> 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<T: AsUrl> FsHash128 for T {
|
||||
fn hash_u128(&self) -> u128 {
|
||||
let mut h = Twox128::default();
|
||||
self.as_url().hash(&mut h);
|
||||
h.finish_128()
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(clean expand path relative);
|
||||
yazi_macro::mod_flat!(clean expand path percent relative);
|
||||
|
|
|
|||
35
yazi-fs/src/path/percent.rs
Normal file
35
yazi-fs/src/path/percent.rs
Normal file
|
|
@ -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() }
|
||||
}
|
||||
|
|
@ -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<PathBuf>;
|
||||
|
||||
fn cache_lock(&self) -> Option<PathBuf>;
|
||||
|
||||
fn cache_root(&self) -> Option<PathBuf>;
|
||||
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
self.cache_root().map(|mut root| {
|
||||
root.push("%lock");
|
||||
root.push(format!("{:x}", self.hash_u128()));
|
||||
root
|
||||
})
|
||||
}
|
||||
|
||||
fn cache_root(&self) -> Option<PathBuf> {
|
||||
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<PathBuf> { self.as_url().cache() }
|
||||
|
||||
fn cache_lock(&self) -> Option<PathBuf> { self.as_url().cache_lock() }
|
||||
|
||||
fn cache_root(&self) -> Option<PathBuf> { 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<PathBuf> { self.as_url().cache() }
|
||||
|
||||
fn cache_lock(&self) -> Option<PathBuf> { self.as_url().cache_lock() }
|
||||
|
||||
fn cache_root(&self) -> Option<PathBuf> { self.as_url().cache_root() }
|
||||
|
||||
fn unified_path(self) -> Cow<'a, Path> {
|
||||
match (self.cache(), self) {
|
||||
(None, UrlCow::Borrowed { loc, .. }) => loc.as_path().into(),
|
||||
|
|
|
|||
|
|
@ -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<PathBuf> = 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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<Function> {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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<Cha> {
|
||||
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<U>(url: U, follow: bool, entry: Option<DirEntry>) -> io::Result<Cha>
|
||||
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<Cha> {
|
||||
Ok(if follow {
|
||||
Cha::from_follow(url, entry.metadata().await?).await
|
||||
} else {
|
||||
entry.metadata().await?
|
||||
})
|
||||
async fn tmp(path: &Path) -> io::Result<PathBuf> {
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Cha>,
|
||||
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<Cha>,
|
||||
}
|
||||
|
||||
impl FileInUpload {
|
||||
pub(super) fn spawn(&self, path: PathBuf, cha: Cha) -> Self {
|
||||
Self { id: self.id, path, cha: Some(cha) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,3 +265,153 @@ impl FileOutTrash {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Download
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum FileOutDownload {
|
||||
New(u64),
|
||||
Deform(String),
|
||||
Succ,
|
||||
Fail(String),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> 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<std::io::Error> for FileOutDownloadDo {
|
||||
fn from(value: std::io::Error) -> Self { Self::Fail(value.to_string()) }
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> 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<std::io::Error> 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<std::io::Error> for FileOutUploadDo {
|
||||
fn from(value: std::io::Error) -> Self { Self::Fail(value.to_string()) }
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,3 +190,99 @@ impl FileProgTrash {
|
|||
|
||||
pub fn percent(self) -> Option<f32> { 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<bool>,
|
||||
}
|
||||
|
||||
impl From<FileProgDownload> 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<f32> {
|
||||
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<bool>,
|
||||
}
|
||||
|
||||
impl From<FileProgUpload> 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<f32> {
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<Child> {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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<TaskProg> 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
|
||||
|
|
|
|||
|
|
@ -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<File>,
|
||||
|
|
@ -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<oneshot::Sender<bool>>) {
|
||||
pub fn file_download(&self, url: UrlBuf, done: Option<oneshot::Sender<bool>>) {
|
||||
let mut ongoing = self.ongoing.lock();
|
||||
let id = ongoing.add::<FileProgPaste>(format!("Download {}", from.display()));
|
||||
let id = ongoing.add::<FileProgDownload>(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
|
||||
|
|
|
|||
|
|
@ -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" ] }
|
||||
|
|
|
|||
|
|
@ -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<Path>,
|
||||
{
|
||||
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<ByteStr<'a>, Error>;
|
||||
}
|
||||
|
||||
impl<'a, T> ToByteStr<'a> for &'a T
|
||||
where
|
||||
T: AsRef<Path> + ?Sized,
|
||||
{
|
||||
fn to_byte_str(self) -> Result<ByteStr<'a>, 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<ByteStr<'a>, Error> { Ok(ByteStr(Cow::Borrowed(&self.0))) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ mod operator;
|
|||
mod packet;
|
||||
mod ser;
|
||||
mod session;
|
||||
#[cfg(windows)]
|
||||
mod wtf;
|
||||
|
||||
pub use byte_str::*;
|
||||
pub(crate) use de::*;
|
||||
|
|
|
|||
|
|
@ -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<Session>);
|
||||
|
||||
|
|
@ -28,9 +28,9 @@ impl Operator {
|
|||
|
||||
pub async fn open<'a, P>(&self, path: P, flags: Flags, attrs: &'a Attrs) -> Result<File, Error>
|
||||
where
|
||||
P: Into<ByteStr<'a>>,
|
||||
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<Attrs, Error>
|
||||
where
|
||||
P: Into<ByteStr<'a>>,
|
||||
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<ByteStr<'a>>,
|
||||
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<ByteStr<'a>>) -> Result<ReadDir, Error> {
|
||||
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<ReadDir, Error>
|
||||
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<ByteStr<'a>>,
|
||||
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<ByteStr<'a>>,
|
||||
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<ByteStr<'a>>,
|
||||
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<PathBuf, Error>
|
||||
where
|
||||
P: Into<ByteStr<'a>>,
|
||||
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<Attrs, Error>
|
||||
where
|
||||
P: Into<ByteStr<'a>>,
|
||||
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<ByteStr<'a>>,
|
||||
T: Into<ByteStr<'a>>,
|
||||
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<PathBuf, Error>
|
||||
where
|
||||
P: Into<ByteStr<'a>>,
|
||||
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<ByteStr<'a>>,
|
||||
L: Into<ByteStr<'a>>,
|
||||
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<ByteStr<'a>>,
|
||||
L: Into<ByteStr<'a>>,
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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<O, L>(original: O, link: L) -> Self
|
||||
pub fn new<O, L>(original: O, link: L) -> Result<Self, Error>
|
||||
where
|
||||
O: Into<ByteStr<'a>>,
|
||||
L: Into<ByteStr<'a>>,
|
||||
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()? })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ByteStr<'a>>) -> Lstat<'a> { Lstat { id: 0, path: path.into() } }
|
||||
pub fn new<'a, P>(path: P) -> Result<Lstat<'a>, 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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<P>(path: P, attrs: Attrs) -> Self
|
||||
pub fn new<P>(path: P, attrs: Attrs) -> Result<Self, Error>
|
||||
where
|
||||
P: Into<ByteStr<'a>>,
|
||||
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() }
|
||||
|
|
|
|||
|
|
@ -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<P>(path: P, flags: Flags, attrs: &'a Attrs) -> Self
|
||||
pub fn new<P>(path: P, flags: Flags, attrs: &'a Attrs) -> Result<Self, Error>
|
||||
where
|
||||
P: Into<ByteStr<'a>>,
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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<ByteStr<'a>>) -> Self { Self { id: 0, path: path.into() } }
|
||||
pub fn new<P>(path: P) -> Result<Self, Error>
|
||||
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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ByteStr<'a>>) -> Self { Self { id: 0, path: path.into() } }
|
||||
pub fn new<P>(path: P) -> Result<Self, Error>
|
||||
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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ByteStr<'a>>) -> Self { Self { id: 0, path: path.into() } }
|
||||
pub fn new<P>(path: P) -> Result<Self, Error>
|
||||
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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ByteStr<'a>>) -> Self { Self { id: 0, path: path.into() } }
|
||||
pub fn new<P>(path: P) -> Result<Self, Error>
|
||||
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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<F, T>(from: F, to: T) -> Self
|
||||
pub fn new<F, T>(from: F, to: T) -> Result<Self, Error>
|
||||
where
|
||||
F: Into<ByteStr<'a>>,
|
||||
T: Into<ByteStr<'a>>,
|
||||
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() }
|
||||
|
|
|
|||
|
|
@ -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<ByteStr<'a>>) -> Self { Self { id: 0, path: path.into() } }
|
||||
pub fn new<P>(path: P) -> Result<Self, Error>
|
||||
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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ByteStr<'a>>, attrs: Attrs) -> Self {
|
||||
Self { id: 0, path: path.into(), attrs }
|
||||
pub fn new<P>(path: P, attrs: Attrs) -> Result<Self, Error>
|
||||
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() }
|
||||
|
|
|
|||
|
|
@ -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<ByteStr<'a>>) -> Self { Self { id: 0, path: path.into() } }
|
||||
pub fn new<P>(path: P) -> Result<Self, Error>
|
||||
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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<L, O>(link: L, original: O) -> Self
|
||||
pub fn new<L, O>(link: L, original: O) -> Result<Self, Error>
|
||||
where
|
||||
L: Into<ByteStr<'a>>,
|
||||
O: Into<ByteStr<'a>>,
|
||||
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 {
|
||||
|
|
|
|||
87
yazi-sftp/src/wtf.rs
Normal file
87
yazi-sftp/src/wtf.rs
Normal file
|
|
@ -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<Vec<u16>> = 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<Cow<'_, [u8]>> {
|
||||
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<u16> = os.encode_wide().collect();
|
||||
assert_eq!(wide, vec![0xdc00 + 0xff]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ impl Hash for Scheme {
|
|||
fn hash<H: Hasher>(&self, state: &mut H) { self.as_ref().hash(state); }
|
||||
}
|
||||
|
||||
impl PartialEq<SchemeRef<'_>> for Scheme {
|
||||
fn eq(&self, other: &SchemeRef<'_>) -> bool { self.as_ref() == *other }
|
||||
}
|
||||
|
||||
impl Scheme {
|
||||
#[inline]
|
||||
pub fn as_ref(&self) -> SchemeRef<'_> { self.into() }
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<Url<'a>> for UrlCow<'a> {
|
||||
|
|
@ -20,9 +20,7 @@ impl<'a> From<Url<'a>> 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<UrlBuf> for UrlCow<'_> {
|
|||
fn from(value: UrlBuf) -> Self { Self::Owned { loc: value.loc, scheme: value.scheme.into() } }
|
||||
}
|
||||
|
||||
impl From<PathBuf> for UrlCow<'_> {
|
||||
fn from(value: PathBuf) -> Self { UrlBuf::from(value).into() }
|
||||
}
|
||||
|
||||
impl From<UrlCow<'_>> for UrlBuf {
|
||||
fn from(value: UrlCow<'_>) -> Self { value.into_owned() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Url<'a>> 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<Url<'a>> 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()) }
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
15
yazi-shim/Cargo.toml
Normal file
15
yazi-shim/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "yazi-shim"
|
||||
version = "25.9.15"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
authors = [ "sxyazi <sxyazi@gmail.com>" ]
|
||||
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 }
|
||||
1
yazi-shim/src/lib.rs
Normal file
1
yazi-shim/src/lib.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
yazi_macro::mod_flat!(twox);
|
||||
|
|
@ -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!() }
|
||||
}
|
||||
|
|
@ -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<UrlBuf> {
|
|||
Ok(url)
|
||||
}
|
||||
|
||||
pub fn copy_with_progress(
|
||||
from: &UrlBuf,
|
||||
to: &UrlBuf,
|
||||
cha: Cha,
|
||||
) -> mpsc::Receiver<Result<u64, io::Error>> {
|
||||
pub fn copy_with_progress<U, V>(from: U, to: V, cha: Cha) -> mpsc::Receiver<Result<u64, io::Error>>
|
||||
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<Result<u64, io::Error>> {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
57
yazi-watcher/src/backend.rs
Normal file
57
yazi-watcher/src/backend.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UrlBuf>) -> 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<UrlBuf>, to_watch: Vec<UrlBuf>) -> 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<I, T>(out_tx: &mpsc::UnboundedSender<UrlBuf>, urls: I)
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<UrlBuf>,
|
||||
{
|
||||
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(()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<dyn notify::Watcher + Send>);
|
||||
|
||||
impl Local {
|
||||
pub(super) fn serve(out_tx: mpsc::UnboundedSender<UrlBuf>) -> Self {
|
||||
let handler = move |res: Result<notify::Event, notify::Error>| {
|
||||
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)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
yazi_macro::mod_flat!(backend local);
|
||||
|
|
@ -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<parking_lot::RwLock<Linked>> = yazi_shared::RoCell::new();
|
||||
pub static WATCHED: yazi_shared::RoCell<parking_lot::RwLock<Watched>> = yazi_shared::RoCell::new();
|
||||
pub static WATCHER: yazi_shared::RoCell<tokio::sync::Semaphore> = yazi_shared::RoCell::new();
|
||||
|
||||
pub fn init() {
|
||||
LINKED.with(<_>::default);
|
||||
WATCHED.with(<_>::default);
|
||||
WATCHER.init(tokio::sync::Semaphore::new(1));
|
||||
|
||||
local::init();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Self>, watched: &'static RwLock<Watched>) {
|
||||
pub(crate) async fn sync(linked: &'static RwLock<Self>, watched: &'static RwLock<Watched>) {
|
||||
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();
|
||||
79
yazi-watcher/src/local/local.rs
Normal file
79
yazi-watcher/src/local/local.rs
Normal file
|
|
@ -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<dyn notify::Watcher + Send>);
|
||||
|
||||
impl Local {
|
||||
pub(crate) fn serve(rx: mpsc::UnboundedReceiver<UrlBuf>, 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<notify::Event, notify::Error>| {
|
||||
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<UrlBuf>) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
5
yazi-watcher/src/local/mod.rs
Normal file
5
yazi-watcher/src/local/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
yazi_macro::mod_flat!(linked local);
|
||||
|
||||
pub static LINKED: yazi_shared::RoCell<parking_lot::RwLock<Linked>> = yazi_shared::RoCell::new();
|
||||
|
||||
pub(super) fn init() { LINKED.with(<_>::default); }
|
||||
1
yazi-watcher/src/remote/mod.rs
Normal file
1
yazi-watcher/src/remote/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
yazi_macro::mod_flat!(remote);
|
||||
40
yazi-watcher/src/remote/remote.rs
Normal file
40
yazi-watcher/src/remote/remote.rs
Normal file
|
|
@ -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<UrlBuf>, _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<UrlBuf>) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
yazi-watcher/src/reporter.rs
Normal file
57
yazi-watcher/src/reporter.rs
Normal file
|
|
@ -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<UrlBuf>,
|
||||
pub(super) remote_tx: mpsc::UnboundedSender<UrlBuf>,
|
||||
}
|
||||
|
||||
impl Reporter {
|
||||
pub(crate) fn report<'a, I>(&self, urls: I)
|
||||
where
|
||||
I: IntoIterator,
|
||||
I::Item: Into<UrlCow<'a>>,
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UrlBuf>);
|
||||
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HashSet<UrlBuf>>,
|
||||
out_tx: mpsc::UnboundedSender<UrlBuf>,
|
||||
tx: watch::Sender<HashSet<UrlBuf>>,
|
||||
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<Item = &'a UrlBuf>) {
|
||||
self.in_tx.send(it.cloned().collect()).ok();
|
||||
pub fn watch<'a, I>(&mut self, urls: I)
|
||||
where
|
||||
I: IntoIterator,
|
||||
I::Item: Into<UrlCow<'a>>,
|
||||
{
|
||||
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<UrlBuf>) { Backend::push_files(&self.out_tx, urls); }
|
||||
pub fn report<'a, I>(&self, urls: I)
|
||||
where
|
||||
I: IntoIterator,
|
||||
I::Item: Into<UrlCow<'a>>,
|
||||
{
|
||||
self.reporter.report(urls);
|
||||
}
|
||||
|
||||
async fn fan_in(mut rx: watch::Receiver<HashSet<UrlBuf>>, mut backend: Backend) {
|
||||
async fn watched(mut rx: watch::Receiver<HashSet<UrlBuf>>, 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<UrlBuf>) {
|
||||
// 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<UrlBuf>, to_watch: Vec<UrlBuf>) -> 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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue