mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
perf: introduce URN to speed up large directory file updates (#1652)
This commit is contained in:
parent
dfe2884110
commit
7222e178a3
20 changed files with 176 additions and 169 deletions
|
|
@ -6,7 +6,7 @@ use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}};
|
|||
use yazi_config::{OPEN, PREVIEW};
|
||||
use yazi_dds::Pubsub;
|
||||
use yazi_proxy::{AppProxy, TasksProxy, HIDER, WATCHER};
|
||||
use yazi_shared::{fs::{max_common_root, maybe_exists, paths_to_same_file, File, FilesOp, Url}, terminal_clear};
|
||||
use yazi_shared::{fs::{max_common_root, maybe_exists, paths_to_same_file, File, Url}, terminal_clear};
|
||||
|
||||
use crate::manager::Manager;
|
||||
|
||||
|
|
@ -98,7 +98,8 @@ impl Manager {
|
|||
// FIXME: consider old and new in the different directories
|
||||
if !succeeded.is_empty() {
|
||||
Pubsub::pub_from_bulk(succeeded.iter().map(|(u, f)| (u, f.url())).collect());
|
||||
FilesOp::Upserting(cwd, succeeded).emit();
|
||||
// FIXME 0
|
||||
// FilesOp::Upserting(cwd, succeeded).emit();
|
||||
}
|
||||
drop(permit);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::fs;
|
||||
use yazi_config::popup::{ConfirmCfg, InputCfg};
|
||||
use yazi_proxy::{ConfirmProxy, InputProxy, TabProxy, WATCHER};
|
||||
use yazi_shared::{event::Cmd, fs::{maybe_exists, ok_or_not_found, symlink_realpath, File, FilesOp, Url}};
|
||||
use yazi_shared::{event::Cmd, fs::{maybe_exists, ok_or_not_found, realname, File, FilesOp, Url, UrnBuf}};
|
||||
|
||||
use crate::manager::Manager;
|
||||
|
||||
|
|
@ -48,9 +48,9 @@ impl Manager {
|
|||
|
||||
if dir {
|
||||
fs::create_dir_all(&new).await?;
|
||||
} else if let Ok(real) = symlink_realpath(&new).await {
|
||||
} else if let Some(real) = realname(&new).await {
|
||||
ok_or_not_found(fs::remove_file(&new).await)?;
|
||||
FilesOp::Deleting(parent.clone(), vec![Url::from(real)]).emit();
|
||||
FilesOp::Deleting(parent.clone(), HashSet::from_iter([UrnBuf::_from(real)])).emit();
|
||||
fs::File::create(&new).await?;
|
||||
} else {
|
||||
fs::create_dir_all(&parent).await.ok();
|
||||
|
|
@ -59,7 +59,7 @@ impl Manager {
|
|||
}
|
||||
|
||||
if let Ok(f) = File::from(new.clone()).await {
|
||||
FilesOp::Upserting(parent, HashMap::from_iter([(f.url_owned(), f)])).emit();
|
||||
FilesOp::Upserting(parent, HashMap::from_iter([(f.urn_owned(), f)])).emit();
|
||||
TabProxy::reveal(&new)
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::fs;
|
||||
use yazi_config::popup::{ConfirmCfg, InputCfg};
|
||||
use yazi_dds::Pubsub;
|
||||
use yazi_proxy::{ConfirmProxy, InputProxy, TabProxy, WATCHER};
|
||||
use yazi_shared::{event::Cmd, fs::{maybe_exists, ok_or_not_found, paths_to_same_file, symlink_realpath, File, FilesOp, Url}};
|
||||
use yazi_shared::{event::Cmd, fs::{maybe_exists, ok_or_not_found, paths_to_same_file, realname, File, FilesOp, Url, UrnBuf}};
|
||||
|
||||
use crate::manager::Manager;
|
||||
|
||||
|
|
@ -74,22 +74,27 @@ impl Manager {
|
|||
}
|
||||
|
||||
async fn rename_do(tab: usize, old: Url, new: Url) -> Result<()> {
|
||||
let Some(p_old) = old.parent_url() else { return Ok(()) };
|
||||
let Some(p_new) = new.parent_url() else { return Ok(()) };
|
||||
let Some((p_old, n_old)) = old.pair() else { return Ok(()) };
|
||||
let Some((p_new, n_new)) = new.pair() else { return Ok(()) };
|
||||
let _permit = WATCHER.acquire().await.unwrap();
|
||||
|
||||
let overwritten = symlink_realpath(&new).await;
|
||||
let overwritten = realname(&new).await;
|
||||
fs::rename(&old, &new).await?;
|
||||
|
||||
if let Ok(o) = overwritten {
|
||||
ok_or_not_found(fs::rename(&o, &new).await)?;
|
||||
FilesOp::Deleting(p_new.clone(), vec![Url::from(o)]).emit();
|
||||
if let Some(o) = overwritten {
|
||||
ok_or_not_found(fs::rename(p_new.join(&o), &new).await)?;
|
||||
FilesOp::Deleting(p_new.clone(), HashSet::from_iter([UrnBuf::_from(o)])).emit();
|
||||
}
|
||||
Pubsub::pub_from_rename(tab, &old, &new);
|
||||
|
||||
let file = File::from(new.clone()).await?;
|
||||
FilesOp::Deleting(p_old, vec![old]).emit();
|
||||
FilesOp::Upserting(p_new, HashMap::from_iter([(new.clone(), file)])).emit();
|
||||
if p_new == p_old {
|
||||
FilesOp::Upserting(p_old, HashMap::from_iter([(n_old, file)])).emit();
|
||||
} else {
|
||||
FilesOp::Deleting(p_old, HashSet::from_iter([n_old])).emit();
|
||||
FilesOp::Upserting(p_new, HashMap::from_iter([(n_new, file)])).emit();
|
||||
}
|
||||
|
||||
Ok(TabProxy::reveal(&new))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ impl Manager {
|
|||
|
||||
let mut ops = vec![opt.op];
|
||||
for u in LINKED.read().from_dir(ops[0].cwd()) {
|
||||
ops.push(ops[0].chroot(u));
|
||||
ops.push(ops[0].rebase(u));
|
||||
}
|
||||
|
||||
for op in ops {
|
||||
|
|
@ -60,10 +60,7 @@ impl Manager {
|
|||
|
||||
fn update_parent(tab: &mut Tab, op: Cow<FilesOp>) {
|
||||
let urn = tab.cwd().urn_owned();
|
||||
// FIXME
|
||||
let leave = false;
|
||||
// let leave = matches!(*op, FilesOp::Deleting(_, ref urls) if
|
||||
// urls.contains(&urn));
|
||||
let leave = matches!(*op, FilesOp::Deleting(_, ref urns) if urns.contains(&urn));
|
||||
|
||||
if let Some(f) = tab.parent.as_mut() {
|
||||
render!(f.update(op.into_owned()));
|
||||
|
|
@ -111,8 +108,8 @@ impl Manager {
|
|||
}
|
||||
|
||||
fn update_history(tab: &mut Tab, op: Cow<FilesOp>) {
|
||||
let leave = tab.parent.as_ref().and_then(|f| f.loc.parent_url().map(|p| (&f.loc, p))).is_some_and(
|
||||
|(p, pp)| matches!(*op, FilesOp::Deleting(ref parent, ref urls) if *parent == pp && urls.contains(p)),
|
||||
let leave = tab.parent.as_ref().and_then(|f| f.loc.parent_url().map(|p| (p, f.loc.urn()))).is_some_and(
|
||||
|(p, n)| matches!(*op, FilesOp::Deleting(ref parent, ref urns) if *parent == p && urns.contains(n)),
|
||||
);
|
||||
|
||||
let folder = tab.history.entry(op.cwd().clone()).or_insert_with(|| Folder::from(op.cwd()));
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use tracing::error;
|
|||
use yazi_fs::{Files, Folder};
|
||||
use yazi_plugin::isolate;
|
||||
use yazi_proxy::WATCHER;
|
||||
use yazi_shared::{fs::{symlink_realname, Cha, File, FilesOp, Url}, RoCell};
|
||||
use yazi_shared::{fs::{realname_unchecked, Cha, File, FilesOp, Url}, RoCell};
|
||||
|
||||
use super::Linked;
|
||||
|
||||
|
|
@ -124,27 +124,26 @@ impl Watcher {
|
|||
let _permit = WATCHER.acquire().await.unwrap();
|
||||
let mut reload = Vec::with_capacity(urls.len());
|
||||
|
||||
for url in urls {
|
||||
let Some(name) = url.file_name() else { continue };
|
||||
let Some(parent) = url.parent_url() else { continue };
|
||||
|
||||
let Ok(file) = File::from(url.clone()).await else {
|
||||
FilesOp::Deleting(parent, vec![url]).emit();
|
||||
for u in urls {
|
||||
let Some((parent, urn)) = u.pair() else { continue };
|
||||
let Ok(file) = File::from(u).await else {
|
||||
FilesOp::Deleting(parent, HashSet::from_iter([urn])).emit();
|
||||
continue;
|
||||
};
|
||||
|
||||
let eq = (!file.is_link() && fs::canonicalize(&url).await.is_ok_and(|p| p == *url))
|
||||
|| symlink_realname(&url, &mut cached).await.is_ok_and(|s| s == name);
|
||||
let u = file.url();
|
||||
let eq = (!file.is_link() && fs::canonicalize(u).await.is_ok_and(|p| p == **u))
|
||||
|| realname_unchecked(u, &mut cached).await.is_ok_and(|s| s == urn._deref()._as_path());
|
||||
|
||||
if !eq {
|
||||
FilesOp::Deleting(parent, vec![url]).emit();
|
||||
FilesOp::Deleting(parent, HashSet::from_iter([urn])).emit();
|
||||
continue;
|
||||
}
|
||||
|
||||
if !file.is_dir() {
|
||||
reload.push(file.clone());
|
||||
}
|
||||
FilesOp::Upserting(parent, HashMap::from_iter([(url, file)])).emit();
|
||||
FilesOp::Upserting(parent, HashMap::from_iter([(urn, file)])).emit();
|
||||
}
|
||||
|
||||
if reload.is_empty() {
|
||||
|
|
|
|||
|
|
@ -39,17 +39,10 @@ impl Yanked {
|
|||
}
|
||||
|
||||
pub fn apply_op(&mut self, op: &FilesOp) {
|
||||
let (removal, addition) = match op {
|
||||
FilesOp::Deleting(_, urls) => (urls.iter().collect(), vec![]),
|
||||
FilesOp::Updating(_, urls) | FilesOp::Upserting(_, urls) => {
|
||||
urls.iter().filter(|(u, _)| self.contains(u)).map(|(u, f)| (u, f.url_owned())).unzip()
|
||||
}
|
||||
_ => (vec![], vec![]),
|
||||
};
|
||||
|
||||
let (removal, addition) = op.diff_recoverable(|u| self.contains(u));
|
||||
if !removal.is_empty() {
|
||||
let old = self.urls.len();
|
||||
self.urls.retain(|u| !removal.contains(&u));
|
||||
self.urls.retain(|u| !removal.contains(u));
|
||||
self.revision += (old != self.urls.len()) as u64;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,23 +18,24 @@ impl Selected {
|
|||
#[inline]
|
||||
pub fn add(&mut self, url: &Url) -> bool { self.add_same(&[url]) == 1 }
|
||||
|
||||
pub fn add_many(&mut self, urls: &[&Url], same: bool) -> usize {
|
||||
pub fn add_many(&mut self, urls: &[impl AsRef<Url>], same: bool) -> usize {
|
||||
if same {
|
||||
return self.add_same(urls);
|
||||
}
|
||||
|
||||
let mut grouped: HashMap<_, Vec<_>> = Default::default();
|
||||
for &u in urls {
|
||||
if let Some(p) = u.parent_url() {
|
||||
for u in urls {
|
||||
if let Some(p) = u.as_ref().parent_url() {
|
||||
grouped.entry(p).or_default().push(u);
|
||||
}
|
||||
}
|
||||
grouped.into_values().map(|v| self.add_same(&v)).sum()
|
||||
}
|
||||
|
||||
fn add_same(&mut self, urls: &[&Url]) -> usize {
|
||||
fn add_same(&mut self, urls: &[impl AsRef<Url>]) -> usize {
|
||||
// If it has appeared as a parent
|
||||
let urls: Vec<_> = urls.iter().filter(|&&u| !self.parents.contains_key(u)).collect();
|
||||
let urls: Vec<_> =
|
||||
urls.iter().map(|u| u.as_ref()).filter(|&u| !self.parents.contains_key(u)).collect();
|
||||
if urls.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -52,7 +53,7 @@ impl Selected {
|
|||
}
|
||||
|
||||
let (now, len) = (timestamp_us(), self.inner.len());
|
||||
self.inner.extend(urls.iter().enumerate().map(|(i, &&u)| (u.clone(), now + i as u64)));
|
||||
self.inner.extend(urls.iter().enumerate().map(|(i, &u)| (u.clone(), now + i as u64)));
|
||||
|
||||
for u in parents {
|
||||
*self.parents.entry(u).or_insert(0) += self.inner.len() - len;
|
||||
|
|
@ -103,14 +104,7 @@ impl Selected {
|
|||
}
|
||||
|
||||
pub fn apply_op(&mut self, op: &FilesOp) {
|
||||
let (removal, addition) = match op {
|
||||
FilesOp::Deleting(_, urls) => (urls.iter().collect(), vec![]),
|
||||
FilesOp::Updating(_, urls) | FilesOp::Upserting(_, urls) => {
|
||||
urls.iter().filter(|&(u, _)| self.contains_key(u)).map(|(u, f)| (u, f.url())).unzip()
|
||||
}
|
||||
_ => (vec![], vec![]),
|
||||
};
|
||||
|
||||
let (removal, addition) = op.diff_recoverable(|u| self.contains_key(u));
|
||||
if !removal.is_empty() {
|
||||
self.remove_many(&removal, !op.cwd().is_search());
|
||||
}
|
||||
|
|
@ -196,7 +190,7 @@ mod tests {
|
|||
fn insert_many_empty_urls_list() {
|
||||
let mut s = Selected::default();
|
||||
|
||||
assert_eq!(0, s.add_same(&[]));
|
||||
assert_eq!(0, s.add_same(&[] as &[&Url]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ impl Tasks {
|
|||
targets
|
||||
.iter()
|
||||
.filter(|f| {
|
||||
f.is_dir() && !targets.sizes.contains_key(f.url()) && !loading.contains(f.url())
|
||||
f.is_dir() && !targets.sizes.contains_key(f.urn()) && !loading.contains(f.url())
|
||||
})
|
||||
.map(|f| f.url())
|
||||
.collect()
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ impl File {
|
|||
|
||||
reg.add_field_method_get("idx", |_, me| Ok(me.idx + 1));
|
||||
reg.add_method("size", |_, me, ()| {
|
||||
Ok(if me.is_dir() { me.folder().files.sizes.get(me.url()).copied() } else { Some(me.len) })
|
||||
Ok(if me.is_dir() { me.folder().files.sizes.get(me.urn()).copied() } else { Some(me.len) })
|
||||
});
|
||||
reg.add_method("mime", |lua, me, ()| {
|
||||
let cx = lua.named_registry_value::<CtxRef>("cx")?;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::{collections::{HashMap, HashSet}, mem, ops::Deref, sync::atomic::Orderi
|
|||
|
||||
use tokio::{fs::{self, DirEntry}, select, sync::mpsc::{self, UnboundedReceiver}};
|
||||
use yazi_config::{manager::SortBy, MANAGER};
|
||||
use yazi_shared::fs::{maybe_exists, Cha, File, FilesOp, Url, Urn, FILES_TICKET};
|
||||
use yazi_shared::fs::{maybe_exists, Cha, File, FilesOp, Url, Urn, UrnBuf, FILES_TICKET};
|
||||
|
||||
use super::{FilesSorter, Filter};
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ pub struct Files {
|
|||
version: u64,
|
||||
pub revision: u64,
|
||||
|
||||
pub sizes: HashMap<Url, u64>,
|
||||
pub sizes: HashMap<UrnBuf, u64>,
|
||||
|
||||
sorter: FilesSorter,
|
||||
filter: Option<Filter>,
|
||||
|
|
@ -99,7 +99,7 @@ impl Files {
|
|||
pub async fn assert_stale(cwd: &Url, cha: Cha) -> Option<Cha> {
|
||||
match fs::metadata(cwd).await.map(Cha::from) {
|
||||
Ok(c) if !c.is_dir() => {
|
||||
// FIXME: use `ErrorKind::NotADirectory` instead once it gets stabilized
|
||||
// TODO: use `ErrorKind::NotADirectory` instead once it gets stabilized
|
||||
FilesOp::IOErr(cwd.clone(), std::io::ErrorKind::AlreadyExists).emit();
|
||||
}
|
||||
Ok(c) if c.hits(cha) => {}
|
||||
|
|
@ -107,8 +107,8 @@ impl Files {
|
|||
Err(e) => {
|
||||
if maybe_exists(cwd).await {
|
||||
FilesOp::IOErr(cwd.clone(), e.kind()).emit();
|
||||
} else if let Some(p) = cwd.parent_url() {
|
||||
FilesOp::Deleting(p, vec![cwd.clone()]).emit();
|
||||
} else if let Some((p, n)) = cwd.pair() {
|
||||
FilesOp::Deleting(p, HashSet::from_iter([n])).emit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -150,7 +150,7 @@ impl Files {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn update_size(&mut self, sizes: HashMap<Url, u64>) {
|
||||
pub fn update_size(&mut self, sizes: HashMap<UrnBuf, u64>) {
|
||||
if sizes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
|
@ -197,31 +197,30 @@ impl Files {
|
|||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn update_deleting(&mut self, urls: Vec<Url>) {
|
||||
if urls.is_empty() {
|
||||
pub fn update_deleting(&mut self, urns: HashSet<UrnBuf>) {
|
||||
if urns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
macro_rules! go {
|
||||
($dist:expr, $src:expr, $inc:literal) => {
|
||||
let mut todo: HashSet<_> = $src.into_iter().collect();
|
||||
let len = $dist.len();
|
||||
|
||||
$dist.retain(|f| !todo.remove(f.url()));
|
||||
$dist.retain(|f| !$src.remove(f.urn()));
|
||||
if $dist.len() != len {
|
||||
self.revision += $inc;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let (hidden, items) = if let Some(filter) = &self.filter {
|
||||
urls.into_iter().partition(|u| {
|
||||
(!self.show_hidden && u.is_hidden()) || !u.file_name().is_some_and(|s| filter.matches(s))
|
||||
let (mut hidden, mut items) = if let Some(filter) = &self.filter {
|
||||
urns.into_iter().partition(|u| {
|
||||
(!self.show_hidden && u._deref().is_hidden())
|
||||
|| !u._deref().name().is_some_and(|s| filter.matches(s))
|
||||
})
|
||||
} else if self.show_hidden {
|
||||
(vec![], urls)
|
||||
(HashSet::new(), urns)
|
||||
} else {
|
||||
urls.into_iter().partition(|u| u.is_hidden())
|
||||
urns.into_iter().partition(|u| u._deref().is_hidden())
|
||||
};
|
||||
|
||||
if !items.is_empty() {
|
||||
|
|
@ -233,31 +232,29 @@ impl Files {
|
|||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn update_deleting(&mut self, urls: Vec<Url>) {
|
||||
pub fn update_deleting(&mut self, mut urns: HashSet<UrnBuf>) {
|
||||
macro_rules! go {
|
||||
($dist:expr, $src:expr, $inc:literal) => {
|
||||
let len = $dist.len();
|
||||
|
||||
$dist.retain(|f| !$src.remove(f.url()));
|
||||
$dist.retain(|f| !$src.remove(f.urn()));
|
||||
if $dist.len() != len {
|
||||
self.revision += $inc;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let mut urls: HashSet<_> = urls.into_iter().collect();
|
||||
if !urls.is_empty() {
|
||||
go!(self.items, urls, 1);
|
||||
if !urns.is_empty() {
|
||||
go!(self.items, urns, 1);
|
||||
}
|
||||
if !urls.is_empty() {
|
||||
go!(self.hidden, urls, 0);
|
||||
if !urns.is_empty() {
|
||||
go!(self.hidden, urns, 0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_updating(
|
||||
&mut self,
|
||||
files: HashMap<Url, File>,
|
||||
) -> (HashMap<Url, File>, HashMap<Url, File>) {
|
||||
files: HashMap<UrnBuf, File>,
|
||||
) -> (HashMap<UrnBuf, File>, HashMap<UrnBuf, File>) {
|
||||
if files.is_empty() {
|
||||
return Default::default();
|
||||
}
|
||||
|
|
@ -266,7 +263,7 @@ impl Files {
|
|||
($dist:expr, $src:expr, $inc:literal) => {
|
||||
let mut b = true;
|
||||
for i in 0..$dist.len() {
|
||||
if let Some(f) = $src.remove($dist[i].url()) {
|
||||
if let Some(f) = $src.remove($dist[i].urn()) {
|
||||
b &= $dist[i].cha.hits(f.cha);
|
||||
$dist[i] = f;
|
||||
|
||||
|
|
@ -298,7 +295,7 @@ impl Files {
|
|||
(hidden, items)
|
||||
}
|
||||
|
||||
pub fn update_upserting(&mut self, files: HashMap<Url, File>) {
|
||||
pub fn update_upserting(&mut self, files: HashMap<UrnBuf, File>) {
|
||||
if files.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ impl Folder {
|
|||
FilesOp::IOErr(..) => self.files.update_ioerr(),
|
||||
|
||||
FilesOp::Creating(_, files) => self.files.update_creating(files),
|
||||
FilesOp::Deleting(_, urls) => self.files.update_deleting(urls),
|
||||
FilesOp::Deleting(_, urns) => self.files.update_deleting(urns),
|
||||
FilesOp::Updating(_, files) => _ = self.files.update_updating(files),
|
||||
FilesOp::Upserting(_, files) => self.files.update_upserting(files),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::{cmp::Ordering, collections::HashMap, mem};
|
||||
|
||||
use yazi_config::manager::SortBy;
|
||||
use yazi_shared::{fs::{File, Url}, natsort, LcgRng, Transliterator};
|
||||
use yazi_shared::{fs::{File, UrnBuf}, natsort, LcgRng, Transliterator};
|
||||
|
||||
#[derive(Clone, Copy, Default, PartialEq)]
|
||||
pub struct FilesSorter {
|
||||
|
|
@ -13,7 +13,7 @@ pub struct FilesSorter {
|
|||
}
|
||||
|
||||
impl FilesSorter {
|
||||
pub(super) fn sort(&self, items: &mut Vec<File>, sizes: &HashMap<Url, u64>) {
|
||||
pub(super) fn sort(&self, items: &mut Vec<File>, sizes: &HashMap<UrnBuf, u64>) {
|
||||
if items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
|
@ -51,8 +51,8 @@ impl FilesSorter {
|
|||
SortBy::Alphabetical => items.sort_unstable_by(by_alphabetical),
|
||||
SortBy::Natural => self.sort_naturally(items),
|
||||
SortBy::Size => items.sort_unstable_by(|a, b| {
|
||||
let aa = if a.is_dir() { sizes.get(a.url()).copied() } else { None };
|
||||
let bb = if b.is_dir() { sizes.get(b.url()).copied() } else { None };
|
||||
let aa = if a.is_dir() { sizes.get(a.urn()).copied() } else { None };
|
||||
let bb = if b.is_dir() { sizes.get(b.urn()).copied() } else { None };
|
||||
let ord = self.cmp(aa.unwrap_or(a.len), bb.unwrap_or(b.len), self.promote(a, b));
|
||||
if ord == Ordering::Equal { by_alphabetical(a, b) } else { ord }
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -111,7 +111,8 @@ pub fn install(lua: &Lua) -> mlua::Result<()> {
|
|||
(
|
||||
"unique_name",
|
||||
lua.create_async_function(|lua, url: UrlRef| async move {
|
||||
// FIXME: handle errors
|
||||
// FIXME: should return a `std::io::Result` to handle errors such as
|
||||
// permission denied
|
||||
Url::cast(lua, yazi_shared::fs::unique_name(url.clone()).await)
|
||||
})?,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -92,7 +92,8 @@ impl Prework {
|
|||
}
|
||||
|
||||
let parent = buf[0].0.parent_url().unwrap();
|
||||
FilesOp::Size(parent, HashMap::from_iter(buf)).emit();
|
||||
// FIXME 0
|
||||
// FilesOp::Size(parent, HashMap::from_iter(buf)).emit();
|
||||
});
|
||||
self.prog.send(TaskProg::Adv(task.id, 1, 0))?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ impl File {
|
|||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
if loc.url().is_hidden() {
|
||||
if loc.urn().is_hidden() {
|
||||
ck |= ChaKind::HIDDEN;
|
||||
}
|
||||
#[cfg(windows)]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::{borrow::Cow, collections::{HashMap, VecDeque}, ffi::{OsStr, OsString}, fs::Metadata, path::{Path, PathBuf}};
|
||||
use std::{borrow::Cow, collections::{HashMap, HashSet, VecDeque}, ffi::{OsStr, OsString}, fs::Metadata, path::{Path, PathBuf}};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use tokio::{fs, io, select, sync::{mpsc, oneshot}, time};
|
||||
|
|
@ -76,19 +76,22 @@ async fn _paths_to_same_file(a: &Path, b: &Path) -> std::io::Result<bool> {
|
|||
Ok(final_name(a).await? == final_name(b).await?)
|
||||
}
|
||||
|
||||
pub async fn symlink_realpath(path: &Path) -> Result<PathBuf> {
|
||||
let p = fs::canonicalize(path).await?;
|
||||
if p == path {
|
||||
return Ok(p);
|
||||
pub async fn realname(p: &Path) -> Option<OsString> {
|
||||
let name = p.file_name()?;
|
||||
if p == fs::canonicalize(p).await.ok()? {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(parent) = path.parent() else { bail!("no parent") };
|
||||
symlink_realname(path, &mut HashMap::new()).await.map(|n| parent.join(n))
|
||||
realname_unchecked(p, &mut HashMap::new())
|
||||
.await
|
||||
.ok()
|
||||
.filter(|s| s != name)
|
||||
.map(|s| s.into_owned())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn test_symlink_realpath() {
|
||||
async fn test_realname_unchecked() {
|
||||
fs::remove_dir_all("/tmp/issue-1173").await.ok();
|
||||
fs::create_dir_all("/tmp/issue-1173/real-dir").await.unwrap();
|
||||
fs::File::create("/tmp/issue-1173/A").await.unwrap();
|
||||
|
|
@ -97,64 +100,52 @@ async fn test_symlink_realpath() {
|
|||
fs::symlink("/tmp/issue-1173/b", "/tmp/issue-1173/D").await.unwrap();
|
||||
fs::symlink("real-dir", "/tmp/issue-1173/link-dir").await.unwrap();
|
||||
|
||||
async fn check(a: &str, b: &str) {
|
||||
let expected = if a == b || cfg!(windows) || cfg!(target_os = "macos") {
|
||||
Some(PathBuf::from(b))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
assert_eq!(symlink_realpath(Path::new(a)).await.ok(), expected);
|
||||
let c = &mut HashMap::new();
|
||||
async fn check(a: &str, b: &str, c: &mut HashMap<PathBuf, HashSet<OsString>>) {
|
||||
assert_eq!(realname_unchecked(Path::new(a), c).await.ok(), Some(OsStr::new(b).into()));
|
||||
}
|
||||
|
||||
check("/tmp/issue-1173/a", "/tmp/issue-1173/A").await;
|
||||
check("/tmp/issue-1173/A", "/tmp/issue-1173/A").await;
|
||||
check("/tmp/issue-1173/a", "A", c).await;
|
||||
check("/tmp/issue-1173/A", "A", c).await;
|
||||
|
||||
check("/tmp/issue-1173/b", "/tmp/issue-1173/b").await;
|
||||
check("/tmp/issue-1173/B", "/tmp/issue-1173/b").await;
|
||||
check("/tmp/issue-1173/b", "b", c).await;
|
||||
check("/tmp/issue-1173/B", "b", c).await;
|
||||
|
||||
check("/tmp/issue-1173/link-dir/c", "/tmp/issue-1173/link-dir/C").await;
|
||||
check("/tmp/issue-1173/link-dir/C", "/tmp/issue-1173/link-dir/C").await;
|
||||
check("/tmp/issue-1173/link-dir/c", "C", c).await;
|
||||
check("/tmp/issue-1173/link-dir/C", "C", c).await;
|
||||
|
||||
check("/tmp/issue-1173/d", "/tmp/issue-1173/D").await;
|
||||
check("/tmp/issue-1173/D", "/tmp/issue-1173/D").await;
|
||||
check("/tmp/issue-1173/d", "D", c).await;
|
||||
check("/tmp/issue-1173/D", "D", c).await;
|
||||
}
|
||||
|
||||
// realpath(3) without resolving symlinks. This is useful for case-insensitive
|
||||
// filesystems.
|
||||
//
|
||||
// Make sure the file of the path exists.
|
||||
pub async fn symlink_realname<'a>(
|
||||
pub async fn realname_unchecked<'a>(
|
||||
path: &'a Path,
|
||||
cached: &'a mut HashMap<PathBuf, HashMap<OsString, OsString>>,
|
||||
cached: &'a mut HashMap<PathBuf, HashSet<OsString>>,
|
||||
) -> Result<Cow<'a, OsStr>> {
|
||||
let Some(name) = path.file_name() else { bail!("no file name") };
|
||||
let Some(parent) = path.parent() else { return Ok(name.into()) };
|
||||
let Some(name) = path.file_name() else { bail!("no filename") };
|
||||
let Some(parent) = path.parent() else { return Ok(Cow::Borrowed(name)) };
|
||||
|
||||
if !cached.contains_key(parent) {
|
||||
let mut map = HashMap::new();
|
||||
let mut set = HashSet::new();
|
||||
let mut it = fs::read_dir(parent).await?;
|
||||
while let Some(entry) = it.next_entry().await? {
|
||||
let n = entry.file_name();
|
||||
if n.as_encoded_bytes().iter().all(|&b| b.is_ascii_lowercase()) {
|
||||
map.insert(n, OsString::new());
|
||||
} else {
|
||||
map.insert(n.to_ascii_lowercase(), n);
|
||||
}
|
||||
set.insert(entry.file_name());
|
||||
}
|
||||
cached.insert(parent.to_owned(), map);
|
||||
cached.insert(parent.to_owned(), set);
|
||||
}
|
||||
|
||||
let c = &cached[parent];
|
||||
if let Some(s) = c.get(name) {
|
||||
return if s.is_empty() { Ok(name.into()) } else { Ok(s.into()) };
|
||||
if c.contains(name) {
|
||||
Ok(Cow::Borrowed(name))
|
||||
} else if let Some(n) = c.iter().find(|&n| n.eq_ignore_ascii_case(name)) {
|
||||
Ok(Cow::Borrowed(n))
|
||||
} else {
|
||||
bail!("no such file")
|
||||
}
|
||||
|
||||
let lowercased = name.to_ascii_lowercase();
|
||||
if let Some(s) = c.get(&lowercased) {
|
||||
return if s.is_empty() { Ok(lowercased.into()) } else { Ok(s.into()) };
|
||||
}
|
||||
|
||||
Ok(name.into())
|
||||
}
|
||||
|
||||
pub async fn calculate_size(path: &Path) -> u64 {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::{collections::HashMap, sync::atomic::{AtomicU64, Ordering}};
|
||||
use std::{collections::{HashMap, HashSet}, sync::atomic::{AtomicU64, Ordering}};
|
||||
|
||||
use super::{Cha, File};
|
||||
use super::{Cha, File, UrnBuf};
|
||||
use crate::{emit, event::Cmd, fs::Url, Layer};
|
||||
|
||||
pub static FILES_TICKET: AtomicU64 = AtomicU64::new(0);
|
||||
|
|
@ -10,13 +10,13 @@ pub enum FilesOp {
|
|||
Full(Url, Vec<File>, Cha),
|
||||
Part(Url, Vec<File>, u64),
|
||||
Done(Url, Cha, u64),
|
||||
Size(Url, HashMap<Url, u64>),
|
||||
Size(Url, HashMap<UrnBuf, u64>),
|
||||
IOErr(Url, std::io::ErrorKind),
|
||||
|
||||
Creating(Url, Vec<File>),
|
||||
Deleting(Url, Vec<Url>),
|
||||
Updating(Url, HashMap<Url, File>),
|
||||
Upserting(Url, HashMap<Url, File>),
|
||||
Deleting(Url, HashSet<UrnBuf>),
|
||||
Updating(Url, HashMap<UrnBuf, File>),
|
||||
Upserting(Url, HashMap<UrnBuf, File>),
|
||||
}
|
||||
|
||||
impl FilesOp {
|
||||
|
|
@ -47,15 +47,12 @@ impl FilesOp {
|
|||
ticket
|
||||
}
|
||||
|
||||
pub fn chroot(&self, new: &Url) -> Self {
|
||||
macro_rules! new {
|
||||
($url:expr) => {{ new.join($url.file_name().unwrap()) }};
|
||||
}
|
||||
pub fn rebase(&self, new: &Url) -> Self {
|
||||
macro_rules! files {
|
||||
($files:expr) => {{ $files.iter().map(|f| f.rebase(new)).collect() }};
|
||||
}
|
||||
macro_rules! map {
|
||||
($map:expr) => {{ $map.iter().map(|(u, f)| (new!(u), f.rebase(new))).collect() }};
|
||||
($map:expr) => {{ $map.iter().map(|(u, f)| (u.clone(), f.rebase(new))).collect() }};
|
||||
}
|
||||
|
||||
let n = new.clone();
|
||||
|
|
@ -63,13 +60,29 @@ impl FilesOp {
|
|||
Self::Full(_, files, mtime) => Self::Full(n, files!(files), *mtime),
|
||||
Self::Part(_, files, ticket) => Self::Part(n, files!(files), *ticket),
|
||||
Self::Done(_, mtime, ticket) => Self::Done(n, *mtime, *ticket),
|
||||
Self::Size(_, map) => Self::Size(n, map.iter().map(|(u, &s)| (new!(u), s)).collect()),
|
||||
Self::Size(_, map) => Self::Size(n, map.iter().map(|(u, &s)| (u.clone(), s)).collect()),
|
||||
Self::IOErr(_, err) => Self::IOErr(n, *err),
|
||||
|
||||
Self::Creating(_, files) => Self::Creating(n, files!(files)),
|
||||
Self::Deleting(_, urls) => Self::Deleting(n, urls.iter().map(|u| new!(u)).collect()),
|
||||
Self::Deleting(_, urns) => Self::Deleting(n, urns.clone()),
|
||||
Self::Updating(_, map) => Self::Updating(n, map!(map)),
|
||||
Self::Upserting(_, map) => Self::Upserting(n, map!(map)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diff_recoverable(&self, contains: impl Fn(&Url) -> bool) -> (Vec<Url>, Vec<Url>) {
|
||||
match self {
|
||||
Self::Deleting(cwd, urns) => {
|
||||
(urns.iter().map(|u| cwd.join(u._deref()._as_path())).collect(), vec![])
|
||||
}
|
||||
Self::Updating(cwd, urns) | Self::Upserting(cwd, urns) => urns
|
||||
.iter()
|
||||
.filter(|&(u, f)| u != f.urn())
|
||||
.map(|(u, f)| (cwd.join(u._deref()._as_path()), f))
|
||||
.filter(|(u, _)| contains(u))
|
||||
.map(|(u, f)| (u, f.url_owned()))
|
||||
.unzip(),
|
||||
_ => (vec![], vec![]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,8 +72,6 @@ fn _expand_path(p: &Path) -> PathBuf {
|
|||
}
|
||||
}
|
||||
|
||||
// FIXME: should return a `std::io::Result` to handle errors such as
|
||||
// permission denied
|
||||
pub async fn unique_name(mut u: Url) -> Url {
|
||||
let Some(stem) = u.file_stem().map(|s| s.to_owned()) else {
|
||||
return u;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::{ffi::{OsStr, OsString}, fmt::{Debug, Display, Formatter}, ops::{Deref,
|
|||
use percent_encoding::{percent_decode_str, percent_encode, AsciiSet, CONTROLS};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::Loc;
|
||||
use super::{Loc, UrnBuf};
|
||||
|
||||
const ENCODE_SET: &AsciiSet = &CONTROLS.add(b'#');
|
||||
|
||||
|
|
@ -150,6 +150,12 @@ impl Url {
|
|||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pair(&self) -> Option<(Self, UrnBuf)> {
|
||||
let urn = UrnBuf::_from(self.path.file_name()?);
|
||||
Some((self.parent_url()?, urn))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn strip_prefix(&self, base: impl AsRef<Path>) -> Option<&Path> {
|
||||
self.path.strip_prefix(base).ok()
|
||||
|
|
@ -158,12 +164,6 @@ impl Url {
|
|||
#[inline]
|
||||
pub fn into_os_string(self) -> OsString { self.path.into_os_string() }
|
||||
|
||||
#[cfg(unix)]
|
||||
#[inline]
|
||||
pub fn is_hidden(&self) -> bool {
|
||||
self.file_name().map_or(false, |s| s.as_encoded_bytes().starts_with(b"."))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_loc(&self, cwd: &Url) -> Loc { self.clone().into_loc(cwd) }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::{borrow::Borrow, path::{Path, PathBuf}};
|
||||
use std::{borrow::Borrow, ffi::OsStr, path::{Path, PathBuf}};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug, Eq, PartialEq, Hash)]
|
||||
#[repr(transparent)]
|
||||
pub struct Urn(Path);
|
||||
|
||||
|
|
@ -10,7 +10,16 @@ impl Urn {
|
|||
unsafe { &*(p.as_ref() as *const Path as *const Self) }
|
||||
}
|
||||
|
||||
// FIXME: remove this
|
||||
#[inline]
|
||||
pub fn name(&self) -> Option<&OsStr> { self.0.file_name() }
|
||||
|
||||
#[cfg(unix)]
|
||||
#[inline]
|
||||
pub fn is_hidden(&self) -> bool {
|
||||
self.name().map_or(false, |s| s.as_encoded_bytes().starts_with(b"."))
|
||||
}
|
||||
|
||||
// FIXME 1: remove this
|
||||
pub fn _as_path(&self) -> &Path { &self.0 }
|
||||
}
|
||||
|
||||
|
|
@ -21,13 +30,21 @@ impl ToOwned for Urn {
|
|||
}
|
||||
|
||||
// --- UrnBuf
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct UrnBuf(PathBuf);
|
||||
|
||||
impl Borrow<Urn> for UrnBuf {
|
||||
fn borrow(&self) -> &Urn { Urn::new(&self.0) }
|
||||
}
|
||||
|
||||
impl UrnBuf {
|
||||
// FIXME: remove this
|
||||
pub fn _deref(&self) -> &Urn { Urn::new(&self.0) }
|
||||
impl PartialEq<Urn> for UrnBuf {
|
||||
fn eq(&self, other: &Urn) -> bool { self.0 == other.0 }
|
||||
}
|
||||
|
||||
impl UrnBuf {
|
||||
// FIXME 1: remove this
|
||||
pub fn _deref(&self) -> &Urn { Urn::new(&self.0) }
|
||||
|
||||
// FIXME 1: remove this
|
||||
pub fn _from(p: impl Into<PathBuf>) -> Self { Self(p.into()) }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue