mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
Compare commits
2 commits
2bd75600ef
...
991a06fbdd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
991a06fbdd | ||
|
|
58f1013348 |
22 changed files with 235 additions and 98 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -4976,6 +4976,7 @@ dependencies = [
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
|
"strum",
|
||||||
"toml",
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"yazi-binding",
|
"yazi-binding",
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use anyhow::Result;
|
||||||
use futures::{StreamExt, stream::FuturesUnordered};
|
use futures::{StreamExt, stream::FuturesUnordered};
|
||||||
use hashbrown::HashSet;
|
use hashbrown::HashSet;
|
||||||
use yazi_core::mgr::OpenOpt;
|
use yazi_core::mgr::OpenOpt;
|
||||||
use yazi_fs::{FsSpec, engine::{Engine, local::Local}};
|
use yazi_fs::{FsAuth, FsUrl, engine::{Engine, local::Local}};
|
||||||
use yazi_macro::succ;
|
use yazi_macro::succ;
|
||||||
use yazi_parser::mgr::DownloadForm;
|
use yazi_parser::mgr::DownloadForm;
|
||||||
use yazi_proxy::MgrProxy;
|
use yazi_proxy::MgrProxy;
|
||||||
|
|
@ -74,10 +74,12 @@ impl Actor for Download {
|
||||||
|
|
||||||
impl Download {
|
impl Download {
|
||||||
async fn prepare(urls: &[UrlBuf]) {
|
async fn prepare(urls: &[UrlBuf]) {
|
||||||
let roots: HashSet<_> = urls.iter().filter_map(|u| u.auth().cache()).collect();
|
let stamp_roots = urls.iter().filter_map(|u| u.auth().stamp_root());
|
||||||
for mut root in roots {
|
let bucket_dirs = urls.iter().filter_map(|u| u.parent()?.cache_bucket());
|
||||||
root.push("%lock");
|
|
||||||
Local::regular(&root).create_dir_all().await.ok();
|
let dirs: HashSet<_> = stamp_roots.chain(bucket_dirs).collect();
|
||||||
|
for dir in dirs {
|
||||||
|
Local::regular(&dir).create_dir_all().await.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -219,9 +219,8 @@ macro_rules! impl_file_fields {
|
||||||
Ok(PathBufDyn::from(me.content_path()))
|
Ok(PathBufDyn::from(me.content_path()))
|
||||||
});
|
});
|
||||||
$fields.add_cached_field("cache", |_, me| {
|
$fields.add_cached_field("cache", |_, me| {
|
||||||
use yazi_fs::FsUrl;
|
|
||||||
use yazi_shared::path::PathBufDyn;
|
use yazi_shared::path::PathBufDyn;
|
||||||
Ok(me.url.cache().map(PathBufDyn::from))
|
Ok(me.cache().map(PathBufDyn::from))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,5 +37,6 @@ ratatui-widgets = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_with = { workspace = true }
|
serde_with = { workspace = true }
|
||||||
|
strum = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ use std::{fmt::Debug, str::FromStr};
|
||||||
|
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use globset::{Candidate, GlobBuilder};
|
use globset::{Candidate, GlobBuilder};
|
||||||
use serde::Deserialize;
|
use serde_with::DeserializeFromStr;
|
||||||
|
use strum::EnumIs;
|
||||||
use yazi_shared::{auth::Auth, url::AsUrl};
|
use yazi_shared::{auth::Auth, url::AsUrl};
|
||||||
|
|
||||||
use crate::Mixable;
|
use crate::Mixable;
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, DeserializeFromStr)]
|
||||||
#[serde(try_from = "String")]
|
|
||||||
pub struct Pattern {
|
pub struct Pattern {
|
||||||
inner: globset::GlobMatcher,
|
inner: globset::GlobMatcher,
|
||||||
scheme: PatternScheme,
|
scheme: PatternScheme,
|
||||||
|
|
@ -98,21 +98,14 @@ impl FromStr for Pattern {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: remove
|
|
||||||
impl TryFrom<String> for Pattern {
|
|
||||||
type Error = anyhow::Error;
|
|
||||||
|
|
||||||
fn try_from(s: String) -> Result<Self, Self::Error> { Self::from_str(s.as_str()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mixable for Pattern {
|
impl Mixable for Pattern {
|
||||||
fn any_file(&self) -> bool { self.is_star && !self.is_dir }
|
fn any_file(&self) -> bool { self.is_star && !self.is_dir && self.scheme.is_any() }
|
||||||
|
|
||||||
fn any_dir(&self) -> bool { self.is_star && self.is_dir }
|
fn any_dir(&self) -> bool { self.is_star && self.is_dir && self.scheme.is_any() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Scheme
|
// --- Scheme
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, EnumIs)]
|
||||||
enum PatternScheme {
|
enum PatternScheme {
|
||||||
Any,
|
Any,
|
||||||
Local,
|
Local,
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,19 @@ use yazi_shim::{mlua::UserDataFieldsExt, strum::IntoStr};
|
||||||
|
|
||||||
use crate::{FsHash128, Xdg};
|
use crate::{FsHash128, Xdg};
|
||||||
|
|
||||||
pub trait FsSpec {
|
pub trait FsAuth {
|
||||||
fn cache(&self) -> Option<PathBuf>;
|
fn cache_root(&self) -> Option<PathBuf>;
|
||||||
|
|
||||||
|
fn stamp_root(&self) -> Option<PathBuf> {
|
||||||
|
self.cache_root().map(|mut root| {
|
||||||
|
root.push("%stamp");
|
||||||
|
root
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FsSpec for Auth {
|
impl FsAuth for Auth {
|
||||||
fn cache(&self) -> Option<PathBuf> {
|
fn cache_root(&self) -> Option<PathBuf> {
|
||||||
match self.kind {
|
match self.kind {
|
||||||
AuthKind::Regular | AuthKind::Search => None,
|
AuthKind::Regular | AuthKind::Search => None,
|
||||||
AuthKind::Mount | AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => {
|
AuthKind::Mount | AuthKind::Hub | AuthKind::Scope | AuthKind::Sftp => {
|
||||||
|
|
@ -18,7 +25,7 @@ impl FsSpec for Auth {
|
||||||
"{}_{}_{}",
|
"{}_{}_{}",
|
||||||
self.kind.into_str(),
|
self.kind.into_str(),
|
||||||
self.scheme,
|
self.scheme,
|
||||||
self.domain.hash_base32(&mut [0; 26])
|
self.domain.hash_u128_str(&mut [0; 26])
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -29,7 +36,8 @@ impl FsSpec for Auth {
|
||||||
inventory::submit! {
|
inventory::submit! {
|
||||||
SpecInventory {
|
SpecInventory {
|
||||||
register: |registry| {
|
register: |registry| {
|
||||||
registry.add_cached_field("cache", |_, me| Ok(me.cache().map(PathBufDyn::from)));
|
registry.add_cached_field("cache", |_, me| Ok(me.cache_root().map(PathBufDyn::from)));
|
||||||
|
registry.add_cached_field("stamp", |_, me| Ok(me.stamp_root().map(PathBufDyn::from)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,9 +67,9 @@ impl UserData for Cha {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
|
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
|
||||||
methods.add_method("hash", |_, me, long: bool| {
|
methods.add_method("hash", |lua, me, long: bool| {
|
||||||
Ok(if long {
|
Ok(if long {
|
||||||
format!("{:x}", me.hash_u128())
|
lua.create_string(me.hash_u128_str(&mut [0; 26]))
|
||||||
} else {
|
} else {
|
||||||
Err("Short hash not supported".into_lua_err())?
|
Err("Short hash not supported".into_lua_err())?
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ impl Cwd {
|
||||||
pub fn ensure(url: Url) -> Cow<Path> {
|
pub fn ensure(url: Url) -> Cow<Path> {
|
||||||
use std::{io::ErrorKind::{AlreadyExists, NotADirectory, NotFound}, path::Component as C};
|
use std::{io::ErrorKind::{AlreadyExists, NotADirectory, NotFound}, path::Component as C};
|
||||||
|
|
||||||
let Some(cache) = url.cache() else {
|
let Some(cache) = url.cache_bucket() else {
|
||||||
return url.working_path();
|
return url.working_path();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{borrow::Cow, ops::Deref, path::Path};
|
use std::{borrow::Cow, ops::Deref, path::{Path, PathBuf}};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use yazi_shared::{path::PathDyn, strand::Strand, url::{AsUrl, Url, UrlBuf, UrlLike}};
|
use yazi_shared::{path::PathDyn, strand::Strand, url::{AsUrl, Url, UrlBuf, UrlLike}};
|
||||||
|
|
@ -40,6 +40,11 @@ impl AsUrl for &File {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl File {
|
impl File {
|
||||||
|
#[inline]
|
||||||
|
pub fn cache(&self) -> Option<PathBuf> {
|
||||||
|
if self.is_dir() { self.url.cache_bucket() } else { self.url.cache_entry() }
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_dummy(url: impl Into<UrlBuf>, r#type: Option<ChaType>) -> Self {
|
pub fn from_dummy(url: impl Into<UrlBuf>, r#type: Option<ChaType>) -> Self {
|
||||||
let url = url.into();
|
let url = url.into();
|
||||||
|
|
@ -59,7 +64,7 @@ impl File {
|
||||||
} else if let Some(local) = self.url.as_local() {
|
} else if let Some(local) = self.url.as_local() {
|
||||||
local.into()
|
local.into()
|
||||||
} else {
|
} else {
|
||||||
self.url.cache().expect("non-local URL should have a cache path").into()
|
self.cache().expect("non-local URL should have a cache path").into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ use crate::{cha::Cha, file::FileSig};
|
||||||
|
|
||||||
pub trait FsHash64 {
|
pub trait FsHash64 {
|
||||||
fn hash_u64(&self) -> u64;
|
fn hash_u64(&self) -> u64;
|
||||||
|
|
||||||
|
fn hash_u64_str<'a>(&self, buf: &'a mut [u8; 13]) -> &'a str {
|
||||||
|
BASE32_NOPAD.encode_mut_str(&self.hash_u64().to_be_bytes(), buf)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FsHash64 for UrlBuf {
|
impl FsHash64 for UrlBuf {
|
||||||
|
|
@ -23,7 +27,7 @@ impl FsHash64 for FileSig<'_> {
|
||||||
pub trait FsHash128 {
|
pub trait FsHash128 {
|
||||||
fn hash_u128(&self) -> u128;
|
fn hash_u128(&self) -> u128;
|
||||||
|
|
||||||
fn hash_base32<'a>(&self, buf: &'a mut [u8; 26]) -> &'a str {
|
fn hash_u128_str<'a>(&self, buf: &'a mut [u8; 26]) -> &'a str {
|
||||||
BASE32_NOPAD.encode_mut_str(&self.hash_u128().to_be_bytes(), buf)
|
BASE32_NOPAD.encode_mut_str(&self.hash_u128().to_be_bytes(), buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -75,11 +79,11 @@ impl FsHash128 for FileSig<'_> {
|
||||||
inventory::submit! {
|
inventory::submit! {
|
||||||
UrlBufInventory {
|
UrlBufInventory {
|
||||||
register: |registry| {
|
register: |registry| {
|
||||||
registry.add_method("hash", |_, me, long: bool| {
|
registry.add_method("hash", |lua, me, long: bool| {
|
||||||
Ok(if long {
|
Ok(if long {
|
||||||
format!("{:x}", me.hash_u128())
|
lua.create_string(me.hash_u128_str(&mut [0; 26]))
|
||||||
} else {
|
} else {
|
||||||
format!("{:x}", me.hash_u64())
|
lua.create_string(me.hash_u64_str(&mut [0; 13]))
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ extern crate self as yazi_fs;
|
||||||
|
|
||||||
yazi_macro::mod_pub!(cha file mounts path engine trash);
|
yazi_macro::mod_pub!(cha file mounts path engine trash);
|
||||||
|
|
||||||
yazi_macro::mod_flat!(cwd spec entries filter fns hash op sorter sorting splatter stage url xdg);
|
yazi_macro::mod_flat!(auth cwd entries filter fns hash op sorter sorting splatter stage url xdg);
|
||||||
|
|
||||||
pub fn init() {
|
pub fn init() {
|
||||||
CWD.init(<_>::default());
|
CWD.init(<_>::default());
|
||||||
|
|
|
||||||
|
|
@ -4,28 +4,38 @@ use mlua::UserDataFields;
|
||||||
use yazi_shared::url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow, UrlLike};
|
use yazi_shared::url::{AsUrl, Url, UrlBuf, UrlBufInventory, UrlCow, UrlLike};
|
||||||
use yazi_shim::mlua::UserDataFieldsExt;
|
use yazi_shim::mlua::UserDataFieldsExt;
|
||||||
|
|
||||||
use crate::{FsHash128, FsSpec};
|
use crate::{FsAuth, FsHash128};
|
||||||
|
|
||||||
pub trait FsUrl<'a> {
|
pub trait FsUrl<'a> {
|
||||||
fn cache(&self) -> Option<PathBuf>;
|
fn cache_bucket(&self) -> Option<PathBuf>;
|
||||||
|
|
||||||
fn cache_lock(&self) -> Option<PathBuf>;
|
fn cache_entry(&self) -> Option<PathBuf>;
|
||||||
|
|
||||||
|
fn stamp_entry(&self) -> Option<PathBuf>;
|
||||||
|
|
||||||
fn working_path(self) -> Cow<'a, Path>;
|
fn working_path(self) -> Cow<'a, Path>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> FsUrl<'a> for Url<'a> {
|
impl<'a> FsUrl<'a> for Url<'a> {
|
||||||
fn cache(&self) -> Option<PathBuf> {
|
fn cache_bucket(&self) -> Option<PathBuf> {
|
||||||
self.auth().cache().map(|mut root| {
|
self.auth().cache_root().map(|mut root| {
|
||||||
root.push(self.hash_base32(&mut [0; 26]));
|
root.push(self.hash_u128_str(&mut [0; 26]));
|
||||||
root
|
root
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cache_lock(&self) -> Option<PathBuf> {
|
fn cache_entry(&self) -> Option<PathBuf> {
|
||||||
self.auth().cache().map(|mut root| {
|
let parent = self.parent()?;
|
||||||
root.push("%lock");
|
parent.auth().cache_root().map(|mut root| {
|
||||||
root.push(self.hash_base32(&mut [0; 26]));
|
root.push(parent.hash_u128_str(&mut [0; 26]));
|
||||||
|
root.push(self.hash_u128_str(&mut [0; 26]));
|
||||||
|
root
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stamp_entry(&self) -> Option<PathBuf> {
|
||||||
|
self.auth().stamp_root().map(|mut root| {
|
||||||
|
root.push(self.hash_u128_str(&mut [0; 26]));
|
||||||
root
|
root
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -34,37 +44,41 @@ impl<'a> FsUrl<'a> for Url<'a> {
|
||||||
match self {
|
match self {
|
||||||
Self::Regular(loc) | Self::Search { loc, .. } => loc.as_inner().into(),
|
Self::Regular(loc) | Self::Search { loc, .. } => loc.as_inner().into(),
|
||||||
Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => {
|
Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => {
|
||||||
self.cache().expect("non-local URL should have a cache path").into()
|
self.cache_bucket().expect("non-local URL should have a cache path").into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FsUrl<'_> for UrlBuf {
|
impl FsUrl<'_> for UrlBuf {
|
||||||
fn cache(&self) -> Option<PathBuf> { self.as_url().cache() }
|
fn cache_bucket(&self) -> Option<PathBuf> { self.as_url().cache_bucket() }
|
||||||
|
|
||||||
fn cache_lock(&self) -> Option<PathBuf> { self.as_url().cache_lock() }
|
fn cache_entry(&self) -> Option<PathBuf> { self.as_url().cache_entry() }
|
||||||
|
|
||||||
|
fn stamp_entry(&self) -> Option<PathBuf> { self.as_url().stamp_entry() }
|
||||||
|
|
||||||
fn working_path(self) -> Cow<'static, Path> {
|
fn working_path(self) -> Cow<'static, Path> {
|
||||||
match self {
|
match self {
|
||||||
Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner().into(),
|
Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner().into(),
|
||||||
Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => {
|
Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => {
|
||||||
self.cache().expect("non-local URL should have a cache path").into()
|
self.cache_bucket().expect("non-local URL should have a cache path").into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> FsUrl<'a> for UrlCow<'a> {
|
impl<'a> FsUrl<'a> for UrlCow<'a> {
|
||||||
fn cache(&self) -> Option<PathBuf> { self.as_url().cache() }
|
fn cache_bucket(&self) -> Option<PathBuf> { self.as_url().cache_bucket() }
|
||||||
|
|
||||||
fn cache_lock(&self) -> Option<PathBuf> { self.as_url().cache_lock() }
|
fn cache_entry(&self) -> Option<PathBuf> { self.as_url().cache_entry() }
|
||||||
|
|
||||||
|
fn stamp_entry(&self) -> Option<PathBuf> { self.as_url().stamp_entry() }
|
||||||
|
|
||||||
fn working_path(self) -> Cow<'a, Path> {
|
fn working_path(self) -> Cow<'a, Path> {
|
||||||
match self {
|
match self {
|
||||||
Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner(),
|
Self::Regular(loc) | Self::Search { loc, .. } => loc.into_inner(),
|
||||||
Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => {
|
Self::Mount { .. } | Self::Hub { .. } | Self::Scope { .. } | Self::Sftp { .. } => {
|
||||||
self.cache().expect("non-local URL should have a cache path").into()
|
self.cache_bucket().expect("non-local URL should have a cache path").into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +88,6 @@ impl<'a> FsUrl<'a> for UrlCow<'a> {
|
||||||
inventory::submit! {
|
inventory::submit! {
|
||||||
UrlBufInventory {
|
UrlBufInventory {
|
||||||
register: |registry| {
|
register: |registry| {
|
||||||
registry.add_cached_field("cache", |_, me| Ok(me.cache()));
|
|
||||||
registry.add_cached_field("domain", |lua, me| {
|
registry.add_cached_field("domain", |lua, me| {
|
||||||
yazi_binding::deprecate!(lua, "{}: `Url.domain` is deprecated, use `Url.spec.domain` instead.");
|
yazi_binding::deprecate!(lua, "{}: `Url.domain` is deprecated, use `Url.spec.domain` instead.");
|
||||||
lua.create_string(&*me.spec().domain)
|
lua.create_string(&*me.spec().domain)
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,16 @@ local M = {}
|
||||||
|
|
||||||
local function stale_cache(file)
|
local function stale_cache(file)
|
||||||
local url = file.url
|
local url = file.url
|
||||||
local lock = url.spec.cache:join(string.format("%%lock/%s", url:hash(true)))
|
local stamp = url.spec.stamp:join(url:hash(true))
|
||||||
|
|
||||||
local fd = fs.access():read(true):open(Url(lock))
|
local fd = fs.access():read(true):open(Url(stamp))
|
||||||
if not fd then
|
if not fd then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
local hash = fd:read(32)
|
local sig = fd:read(26)
|
||||||
ya.drop(fd)
|
ya.drop(fd)
|
||||||
return hash ~= file.cha:hash(true)
|
return sig ~= file.cha:hash(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
function M:fetch(job)
|
function M:fetch(job)
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ impl Utils {
|
||||||
}
|
}
|
||||||
|
|
||||||
let sig = Sig(FileSig(f), t.raw_get("skip").unwrap_or_default());
|
let sig = Sig(FileSig(f), t.raw_get("skip").unwrap_or_default());
|
||||||
Ok(Some(UrlBuf::from(YAZI.preview.cache_dir.join(sig.hash_base32(&mut [0; 26])))))
|
Ok(Some(UrlBuf::from(YAZI.preview.cache_dir.join(sig.hash_u128_str(&mut [0; 26])))))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use tracing::warn;
|
||||||
use yazi_config::YAZI;
|
use yazi_config::YAZI;
|
||||||
use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, engine::{Attrs, Engine, FileHolder, local::Local}, ok_or_not_found, path::path_relative_to};
|
use yazi_fs::{Cwd, FsHash128, FsUrl, cha::Cha, engine::{Attrs, Engine, FileHolder, local::Local}, ok_or_not_found, path::path_relative_to};
|
||||||
use yazi_shared::{path::PathCow, url::{AsUrl, UrlCow, UrlLike}};
|
use yazi_shared::{path::PathCow, url::{AsUrl, UrlCow, UrlLike}};
|
||||||
use yazi_vfs::{VfsCha, engine::{self, DirEntry}, maybe_exists, unique_file};
|
use yazi_vfs::{Stamp, VfsCha, engine::{self, DirEntry}, maybe_exists, unique_file};
|
||||||
|
|
||||||
use super::{FileInCopy, FileInDelete, FileInHardlink, FileInLink, FileInTrash};
|
use super::{FileInCopy, FileInDelete, FileInHardlink, FileInLink, FileInTrash};
|
||||||
use crate::{LOW, NORMAL, TaskOp, TaskOps, TasksProxy, ctx, file::{FileIn, FileInCut, FileInDownload, FileInUpload, FileOutCopy, FileOutCopyDo, FileOutCut, FileOutCutDo, FileOutDelete, FileOutDeleteDo, FileOutDownload, FileOutDownloadDo, FileOutHardlink, FileOutHardlinkDo, FileOutLink, FileOutTrash, FileOutUpload, FileOutUploadDo, Transaction, Traverse}, hook::{HookInOutCopy, HookInOutCut, HookInOutHardlink, HookInOutLink}, ok_or_not_found};
|
use crate::{LOW, NORMAL, TaskOp, TaskOps, TasksProxy, ctx, file::{FileIn, FileInCut, FileInDownload, FileInUpload, FileOutCopy, FileOutCopyDo, FileOutCut, FileOutCutDo, FileOutDelete, FileOutDeleteDo, FileOutDownload, FileOutDownloadDo, FileOutHardlink, FileOutHardlinkDo, FileOutLink, FileOutTrash, FileOutUpload, FileOutUploadDo, Transaction, Traverse}, hook::{HookInOutCopy, HookInOutCut, HookInOutHardlink, HookInOutLink}, ok_or_not_found};
|
||||||
|
|
@ -342,7 +342,7 @@ impl File {
|
||||||
) -> Result<(), FileOutDownloadDo> {
|
) -> Result<(), FileOutDownloadDo> {
|
||||||
let cha = task.cha.unwrap();
|
let cha = task.cha.unwrap();
|
||||||
|
|
||||||
let cache = ctx!(task, task.target.cache(), "Cannot determine cache path")?;
|
let cache = ctx!(task, task.target.cache_entry(), "Cannot determine cache path")?;
|
||||||
let cache_tmp = ctx!(task, Transaction::tmp(&cache).await, "Cannot determine download cache")?;
|
let cache_tmp = ctx!(task, Transaction::tmp(&cache).await, "Cannot determine download cache")?;
|
||||||
|
|
||||||
let mut rx = ctx!(task, engine::copy_progressive(&task.target, &cache_tmp, cha).await)?;
|
let mut rx = ctx!(task, engine::copy_progressive(&task.target, &cache_tmp, cha).await)?;
|
||||||
|
|
@ -350,12 +350,8 @@ impl File {
|
||||||
match rx.recv().await.unwrap_or(Ok(0)) {
|
match rx.recv().await.unwrap_or(Ok(0)) {
|
||||||
Ok(0) => {
|
Ok(0) => {
|
||||||
Local::regular(&cache).remove_dir_all().await.ok();
|
Local::regular(&cache).remove_dir_all().await.ok();
|
||||||
|
ctx!(task, Stamp::write(cha, task.target.as_url()).await)?;
|
||||||
ctx!(task, engine::rename(cache_tmp, cache).await, "Cannot persist downloaded file")?;
|
ctx!(task, engine::rename(cache_tmp, cache).await, "Cannot persist downloaded file")?;
|
||||||
|
|
||||||
let lock = ctx!(task, task.target.cache_lock(), "Cannot determine cache lock")?;
|
|
||||||
let hash = format!("{:x}", cha.hash_u128());
|
|
||||||
ctx!(task, Local::regular(&lock).write(hash).await, "Cannot lock cache")?;
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Ok(n) => self.ops.out(task.id, FileOutDownloadDo::Adv(n)),
|
Ok(n) => self.ops.out(task.id, FileOutDownloadDo::Adv(n)),
|
||||||
|
|
@ -410,13 +406,9 @@ impl File {
|
||||||
pub(crate) async fn upload_do(&self, task: FileInUpload) -> Result<(), FileOutUploadDo> {
|
pub(crate) async fn upload_do(&self, task: FileInUpload) -> Result<(), FileOutUploadDo> {
|
||||||
let cha = task.cha.unwrap();
|
let cha = task.cha.unwrap();
|
||||||
let cache = ctx!(task, task.cache.as_ref(), "Cannot determine cache path")?;
|
let cache = ctx!(task, task.cache.as_ref(), "Cannot determine cache path")?;
|
||||||
let lock = ctx!(task, task.target.cache_lock(), "Cannot determine cache lock")?;
|
|
||||||
|
|
||||||
let hash = ctx!(task, Local::regular(&lock).read_to_string().await, "Cannot read cache lock")?;
|
let stamp = ctx!(task, Stamp::read(&task.target).await)?;
|
||||||
let hash = ctx!(task, u128::from_str_radix(&hash, 16), "Cannot parse hash from lock")?;
|
ctx!(task, stamp.validate(cha, task.target.as_url()))?;
|
||||||
if hash != cha.hash_u128() {
|
|
||||||
Err(anyhow!("Failed to work on: {task:?}: remote file has changed since last download"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tmp =
|
let tmp =
|
||||||
ctx!(task, Transaction::tmp(&task.target).await, "Cannot determine temporary upload path")?;
|
ctx!(task, Transaction::tmp(&task.target).await, "Cannot determine temporary upload path")?;
|
||||||
|
|
@ -436,7 +428,7 @@ impl File {
|
||||||
Ok(0) => {
|
Ok(0) => {
|
||||||
let cha =
|
let cha =
|
||||||
ctx!(task, Self::cha(&task.target, true, None).await, "Cannot stat original file")?;
|
ctx!(task, Self::cha(&task.target, true, None).await, "Cannot stat original file")?;
|
||||||
if hash != cha.hash_u128() {
|
if stamp.sig() != cha.hash_u128_str(&mut [0; 26]) {
|
||||||
Err(anyhow!("Failed to work on: {task:?}: remote file has changed during upload"))?;
|
Err(anyhow!("Failed to work on: {task:?}: remote file has changed during upload"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -444,8 +436,7 @@ impl File {
|
||||||
|
|
||||||
let cha =
|
let cha =
|
||||||
ctx!(task, Self::cha(&task.target, true, None).await, "Cannot stat uploaded file")?;
|
ctx!(task, Self::cha(&task.target, true, None).await, "Cannot stat uploaded file")?;
|
||||||
let hash = format!("{:x}", cha.hash_u128());
|
ctx!(task, Stamp::write(cha, task.target.as_url()).await)?;
|
||||||
ctx!(task, Local::regular(&lock).write(hash).await, "Cannot lock cache")?;
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -131,13 +131,13 @@ impl Traverse for FileInUpload {
|
||||||
self.cha = Some(super::File::cha(self.from(), self.follow(), None).await?)
|
self.cha = Some(super::File::cha(self.from(), self.follow(), None).await?)
|
||||||
}
|
}
|
||||||
if self.cache.is_none() {
|
if self.cache.is_none() {
|
||||||
self.cache = self.target.cache();
|
self.cache = self.target.cache_entry();
|
||||||
}
|
}
|
||||||
Ok(self.cha.unwrap())
|
Ok(self.cha.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn(&self, from: UrlBuf, _to: Option<UrlBuf>, cha: Cha) -> Self {
|
fn spawn(&self, from: UrlBuf, _to: Option<UrlBuf>, cha: Cha) -> Self {
|
||||||
Self { id: self.id, cha: Some(cha), cache: from.cache(), target: from }
|
Self { id: self.id, cha: Some(cha), cache: from.cache_entry(), target: from }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to(&self) -> Option<Url<'_>> { None }
|
fn to(&self) -> Option<Url<'_>> { None }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
yazi_macro::mod_pub!(engine);
|
yazi_macro::mod_pub!(engine);
|
||||||
|
|
||||||
yazi_macro::mod_flat!(cha entries file fns);
|
yazi_macro::mod_flat!(cha entries file fns stamp);
|
||||||
|
|
||||||
pub fn init() { engine::init(); }
|
pub fn init() { engine::init(); }
|
||||||
|
|
|
||||||
108
yazi-vfs/src/stamp.rs
Normal file
108
yazi-vfs/src/stamp.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
use std::{io, path::Path, str};
|
||||||
|
|
||||||
|
use yazi_fs::{FsAuth, FsHash128, FsUrl, cha::Cha, engine::{Engine, local::Local}};
|
||||||
|
use yazi_shared::{strand::{AsStrand, StrandCow}, url::{AsUrl, Url, UrlBuf}};
|
||||||
|
|
||||||
|
pub struct Stamp(Vec<u8>);
|
||||||
|
|
||||||
|
impl Stamp {
|
||||||
|
const SIG_LEN: usize = 26;
|
||||||
|
|
||||||
|
pub async fn read<U>(url: U) -> io::Result<Self>
|
||||||
|
where
|
||||||
|
U: AsUrl,
|
||||||
|
{
|
||||||
|
let path =
|
||||||
|
url.as_url().stamp_entry().ok_or_else(|| io::Error::other("Cannot determine cache stamp"))?;
|
||||||
|
|
||||||
|
Self::read_at(&path).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_at(path: &Path) -> io::Result<Self> {
|
||||||
|
let data = Local::regular(path)
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.map_err(|e| io::Error::new(e.kind(), format!("Cannot read cache stamp: {e}")))?;
|
||||||
|
|
||||||
|
Self::try_from(data)
|
||||||
|
.map_err(|e| io::Error::new(e.kind(), format!("Cannot parse cache stamp: {e}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn resolve<U, S>(dir: U, key: S) -> io::Result<UrlBuf>
|
||||||
|
where
|
||||||
|
U: AsUrl,
|
||||||
|
S: AsStrand,
|
||||||
|
{
|
||||||
|
let dir = dir.as_url();
|
||||||
|
let key = key.as_strand();
|
||||||
|
|
||||||
|
let mut path =
|
||||||
|
dir.auth().stamp_root().ok_or_else(|| io::Error::other("Cannot determine stamp root"))?;
|
||||||
|
path.push(key.as_os()?);
|
||||||
|
|
||||||
|
let stamp = Self::read_at(&path).await?;
|
||||||
|
let name = StrandCow::with(dir.kind(), stamp.name()).map_err(io::Error::other)?;
|
||||||
|
|
||||||
|
let url = dir.try_join(name)?;
|
||||||
|
if url.hash_u128_str(&mut [0; Self::SIG_LEN]).as_bytes() != key.encoded_bytes() {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::InvalidData, "Cache stamp does not match entry"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write(cha: Cha, url: Url<'_>) -> io::Result<()> {
|
||||||
|
let path = url.stamp_entry().ok_or_else(|| io::Error::other("Cannot determine cache stamp"))?;
|
||||||
|
let data = Self::encode(cha, url)?;
|
||||||
|
|
||||||
|
Local::regular(&path)
|
||||||
|
.write(data)
|
||||||
|
.await
|
||||||
|
.map_err(|e| io::Error::new(e.kind(), format!("Cannot write cache stamp: {e}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode(cha: Cha, url: Url) -> io::Result<Vec<u8>> {
|
||||||
|
let name = url.name().ok_or_else(|| io::Error::other("URL has no filename"))?;
|
||||||
|
|
||||||
|
let mut buf = Vec::with_capacity(Self::SIG_LEN + name.len());
|
||||||
|
buf.extend_from_slice(cha.hash_u128_str(&mut [0; Self::SIG_LEN]).as_bytes());
|
||||||
|
buf.extend_from_slice(name.encoded_bytes());
|
||||||
|
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(&self, cha: Cha, url: Url) -> io::Result<()> {
|
||||||
|
let name = url.name().ok_or_else(|| io::Error::other("URL has no filename"))?;
|
||||||
|
if self.name() != name.encoded_bytes() {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::InvalidData, "Cache stamp does not match target"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.sig() != cha.hash_u128_str(&mut [0; Self::SIG_LEN]) {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"Remote file has changed since last download",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn sig(&self) -> &str { unsafe { str::from_utf8_unchecked(&self.0[..Self::SIG_LEN]) } }
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn name(&self) -> &[u8] { &self.0[Self::SIG_LEN..] }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Vec<u8>> for Stamp {
|
||||||
|
type Error = io::Error;
|
||||||
|
|
||||||
|
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||||
|
let (sig, _) = value
|
||||||
|
.split_at_checked(Self::SIG_LEN)
|
||||||
|
.filter(|(_, n)| !n.is_empty())
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Invalid cache stamp"))?;
|
||||||
|
|
||||||
|
str::from_utf8(sig).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||||
|
Ok(Self(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use yazi_shared::{auth::AuthKind, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}};
|
use yazi_shared::{auth::AuthKind, url::{AsUrl, Url, UrlBuf, UrlCow, UrlLike}};
|
||||||
|
|
||||||
use crate::{WATCHED, local::LINKED};
|
use crate::{WATCHED, local::LINKED, r#virtual::VirtualReport};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct Reporter {
|
pub(crate) struct Reporter {
|
||||||
pub(super) local_tx: mpsc::UnboundedSender<UrlBuf>,
|
pub(super) local_tx: mpsc::UnboundedSender<UrlBuf>,
|
||||||
pub(super) virtual_tx: mpsc::UnboundedSender<(UrlBuf, bool)>,
|
pub(super) virtual_tx: mpsc::UnboundedSender<VirtualReport>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Reporter {
|
impl Reporter {
|
||||||
|
|
@ -45,11 +45,8 @@ impl Reporter {
|
||||||
|
|
||||||
// Virtual caches
|
// Virtual caches
|
||||||
let Some(dir) = watched.find_by_cache(parent.loc()) else { continue };
|
let Some(dir) = watched.find_by_cache(parent.loc()) else { continue };
|
||||||
let Some(name) = url.name() else { continue };
|
let Some(key) = url.name() else { continue };
|
||||||
if let Ok(u) = dir.try_join(name) {
|
self.virtual_tx.send(VirtualReport::Cache(dir, key.to_owned())).ok();
|
||||||
self.virtual_tx.send((u, true)).ok();
|
|
||||||
}
|
|
||||||
self.virtual_tx.send((dir, false)).ok();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,7 +56,7 @@ impl Reporter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.virtual_tx.send((parent.to_owned(), false)).ok();
|
self.virtual_tx.send(VirtualReport::Url(parent.to_owned())).ok();
|
||||||
self.virtual_tx.send((url.into_owned(), false)).ok();
|
self.virtual_tx.send(VirtualReport::Url(url.into_owned())).ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,25 @@
|
||||||
use std::{io, time::{Duration, SystemTime}};
|
use std::{io, time::{Duration, SystemTime}};
|
||||||
|
|
||||||
use hashbrown::HashMap;
|
use hashbrown::{HashMap, HashSet};
|
||||||
use notify::Result;
|
use notify::Result;
|
||||||
use tokio::{pin, sync::mpsc::UnboundedReceiver};
|
use tokio::{pin, sync::mpsc::UnboundedReceiver};
|
||||||
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
|
use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream};
|
||||||
use yazi_fs::FilesOp;
|
use yazi_fs::FilesOp;
|
||||||
use yazi_shared::url::{UrlBuf, UrlLike};
|
use yazi_shared::{strand::StrandBuf, url::{UrlBuf, UrlLike}};
|
||||||
use yazi_vfs::engine;
|
use yazi_vfs::{Stamp, engine};
|
||||||
|
|
||||||
use crate::{MgrProxy, WATCHER, Watchee};
|
use crate::{MgrProxy, WATCHER, Watchee};
|
||||||
|
|
||||||
pub(crate) struct Virtual;
|
pub(crate) struct Virtual;
|
||||||
|
|
||||||
|
#[derive(Hash, PartialEq, Eq)]
|
||||||
|
pub(crate) enum VirtualReport {
|
||||||
|
Url(UrlBuf),
|
||||||
|
Cache(UrlBuf, StrandBuf),
|
||||||
|
}
|
||||||
|
|
||||||
impl Virtual {
|
impl Virtual {
|
||||||
pub(crate) fn serve(rx: UnboundedReceiver<(UrlBuf, bool)>) -> Self {
|
pub(crate) fn serve(rx: UnboundedReceiver<VirtualReport>) -> Self {
|
||||||
tokio::spawn(Self::changed(rx));
|
tokio::spawn(Self::changed(rx));
|
||||||
|
|
||||||
Self
|
Self
|
||||||
|
|
@ -23,14 +29,23 @@ impl Virtual {
|
||||||
|
|
||||||
pub(crate) fn unwatch(&mut self, _watchee: &Watchee) -> Result<()> { Ok(()) }
|
pub(crate) fn unwatch(&mut self, _watchee: &Watchee) -> Result<()> { Ok(()) }
|
||||||
|
|
||||||
async fn changed(rx: UnboundedReceiver<(UrlBuf, bool)>) {
|
async fn changed(rx: UnboundedReceiver<VirtualReport>) {
|
||||||
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(250));
|
let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(250));
|
||||||
pin!(rx);
|
pin!(rx);
|
||||||
|
|
||||||
while let Some(chunk) = rx.next().await {
|
while let Some(chunk) = rx.next().await {
|
||||||
let mut urls = HashMap::with_capacity(chunk.len());
|
let reports: HashSet<_> = chunk.into_iter().collect();
|
||||||
for (u, upload) in chunk {
|
|
||||||
urls.entry(u).and_modify(|b| *b |= upload).or_insert(upload);
|
let mut urls = HashMap::with_capacity(reports.len());
|
||||||
|
for report in reports {
|
||||||
|
match report {
|
||||||
|
VirtualReport::Url(url) => _ = urls.entry(url).or_insert(false),
|
||||||
|
VirtualReport::Cache(dir, key) if let Ok(file) = Stamp::resolve(&dir, &key).await => {
|
||||||
|
urls.insert(file, true);
|
||||||
|
urls.entry(dir).or_insert(false);
|
||||||
|
}
|
||||||
|
VirtualReport::Cache(..) => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _permit = WATCHER.acquire().await.unwrap();
|
let _permit = WATCHER.acquire().await.unwrap();
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ impl Deref for Watched {
|
||||||
impl Watched {
|
impl Watched {
|
||||||
pub(super) fn insert(&mut self, watchee: Watchee<'static>) {
|
pub(super) fn insert(&mut self, watchee: Watchee<'static>) {
|
||||||
let url = watchee.as_url();
|
let url = watchee.as_url();
|
||||||
if let Some(cache) = url.cache() {
|
if let Some(cache) = url.cache_bucket() {
|
||||||
self.caches.insert(cache, url.into());
|
self.caches.insert(cache, url.into());
|
||||||
}
|
}
|
||||||
self.urls.insert(watchee);
|
self.urls.insert(watchee);
|
||||||
|
|
@ -32,7 +32,7 @@ impl Watched {
|
||||||
if !self.urls.remove(watchee) {
|
if !self.urls.remove(watchee) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(cache) = watchee.as_url().cache() {
|
if let Some(cache) = watchee.as_url().cache_bucket() {
|
||||||
self.caches.remove(&cache);
|
self.caches.remove(&cache);
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ impl Watcher {
|
||||||
for file in it.map(Into::into) {
|
for file in it.map(Into::into) {
|
||||||
if !file.url.is_absolute() {
|
if !file.url.is_absolute() {
|
||||||
continue;
|
continue;
|
||||||
} else if let Some(cache) = file.url.cache() {
|
} else if let Some(cache) = file.url.cache_bucket() {
|
||||||
urls.insert(cache.into());
|
urls.insert(cache.into());
|
||||||
}
|
}
|
||||||
urls.insert(file.url.clone());
|
urls.insert(file.url.clone());
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue