Compare commits

..

2 commits

Author SHA1 Message Date
dev_Hakaze
0ee3167250
Merge 751d5729bc into 58f1013348 2026-07-23 02:45:21 +08:00
三咲雅 misaki masa
58f1013348
feat: trash bin (#4144)
Some checks failed
Cachix / Publish Flake (push) Has been cancelled
Check / clippy (push) Has been cancelled
Check / rustfmt (push) Has been cancelled
Check / stylua (push) Has been cancelled
Draft / build-unix (gcc-aarch64-linux-gnu, ubuntu-latest, aarch64-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (gcc-i686-linux-gnu, ubuntu-latest, i686-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (gcc-riscv64-linux-gnu, ubuntu-latest, riscv64gc-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (gcc-sparc64-linux-gnu, ubuntu-latest, sparc64-unknown-linux-gnu) (push) Has been cancelled
Draft / build-unix (macos-latest, aarch64-apple-darwin) (push) Has been cancelled
Draft / build-unix (macos-latest, x86_64-apple-darwin) (push) Has been cancelled
Draft / build-unix (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Has been cancelled
Draft / build-windows (windows-latest, aarch64-pc-windows-msvc) (push) Has been cancelled
Draft / build-windows (windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
Draft / build-musl (aarch64-unknown-linux-musl) (push) Has been cancelled
Draft / build-musl (x86_64-unknown-linux-musl) (push) Has been cancelled
Draft / build-snap (amd64, ubuntu-latest) (push) Has been cancelled
Draft / build-snap (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Test / test (macos-latest) (push) Has been cancelled
Test / test (ubuntu-latest) (push) Has been cancelled
Test / test (windows-latest) (push) Has been cancelled
Draft / snap (push) Has been cancelled
Draft / draft (push) Has been cancelled
Draft / nightly (push) Has been cancelled
2026-07-23 01:59:39 +08:00
22 changed files with 235 additions and 98 deletions

1
Cargo.lock generated
View file

@ -4976,6 +4976,7 @@ dependencies = [
"regex", "regex",
"serde", "serde",
"serde_with", "serde_with",
"strum",
"toml", "toml",
"tracing", "tracing",
"yazi-binding", "yazi-binding",

View file

@ -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();
} }
} }
} }

View file

@ -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))
}); });
}; };
} }

View file

@ -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 }

View file

@ -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,

View file

@ -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)));
} }
} }
} }

View file

@ -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())?
}) })

View file

@ -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();
}; };

View file

@ -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()
} }
} }
} }

View file

@ -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]))
}) })
}); });
} }

View file

@ -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());

View file

@ -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)

View file

@ -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)

View file

@ -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])))))
}) })
}) })
} }

View file

@ -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;
} }

View file

@ -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 }

View file

@ -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
View 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))
}
}

View file

@ -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();
} }
} }

View file

@ -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();

View file

@ -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

View file

@ -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());