perf: introduce URN to speed up large directory file updates (#1652)

This commit is contained in:
三咲雅 · Misaki Masa 2024-09-19 20:43:03 +08:00 committed by GitHub
parent dfe2884110
commit 7222e178a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 176 additions and 169 deletions

View file

@ -6,7 +6,7 @@ use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}};
use yazi_config::{OPEN, PREVIEW}; use yazi_config::{OPEN, PREVIEW};
use yazi_dds::Pubsub; use yazi_dds::Pubsub;
use yazi_proxy::{AppProxy, TasksProxy, HIDER, WATCHER}; 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; use crate::manager::Manager;
@ -98,7 +98,8 @@ impl Manager {
// FIXME: consider old and new in the different directories // FIXME: consider old and new in the different directories
if !succeeded.is_empty() { if !succeeded.is_empty() {
Pubsub::pub_from_bulk(succeeded.iter().map(|(u, f)| (u, f.url())).collect()); 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); drop(permit);

View file

@ -1,10 +1,10 @@
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use anyhow::Result; use anyhow::Result;
use tokio::fs; use tokio::fs;
use yazi_config::popup::{ConfirmCfg, InputCfg}; use yazi_config::popup::{ConfirmCfg, InputCfg};
use yazi_proxy::{ConfirmProxy, InputProxy, TabProxy, WATCHER}; 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; use crate::manager::Manager;
@ -48,9 +48,9 @@ impl Manager {
if dir { if dir {
fs::create_dir_all(&new).await?; 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)?; 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?; fs::File::create(&new).await?;
} else { } else {
fs::create_dir_all(&parent).await.ok(); fs::create_dir_all(&parent).await.ok();
@ -59,7 +59,7 @@ impl Manager {
} }
if let Ok(f) = File::from(new.clone()).await { 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) TabProxy::reveal(&new)
} }
Ok(()) Ok(())

View file

@ -1,11 +1,11 @@
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use anyhow::Result; use anyhow::Result;
use tokio::fs; use tokio::fs;
use yazi_config::popup::{ConfirmCfg, InputCfg}; use yazi_config::popup::{ConfirmCfg, InputCfg};
use yazi_dds::Pubsub; use yazi_dds::Pubsub;
use yazi_proxy::{ConfirmProxy, InputProxy, TabProxy, WATCHER}; 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; use crate::manager::Manager;
@ -74,22 +74,27 @@ impl Manager {
} }
async fn rename_do(tab: usize, old: Url, new: Url) -> Result<()> { async fn rename_do(tab: usize, old: Url, new: Url) -> Result<()> {
let Some(p_old) = old.parent_url() else { return Ok(()) }; let Some((p_old, n_old)) = old.pair() else { return Ok(()) };
let Some(p_new) = new.parent_url() else { return Ok(()) }; let Some((p_new, n_new)) = new.pair() else { return Ok(()) };
let _permit = WATCHER.acquire().await.unwrap(); let _permit = WATCHER.acquire().await.unwrap();
let overwritten = symlink_realpath(&new).await; let overwritten = realname(&new).await;
fs::rename(&old, &new).await?; fs::rename(&old, &new).await?;
if let Ok(o) = overwritten { if let Some(o) = overwritten {
ok_or_not_found(fs::rename(&o, &new).await)?; ok_or_not_found(fs::rename(p_new.join(&o), &new).await)?;
FilesOp::Deleting(p_new.clone(), vec![Url::from(o)]).emit(); FilesOp::Deleting(p_new.clone(), HashSet::from_iter([UrnBuf::_from(o)])).emit();
} }
Pubsub::pub_from_rename(tab, &old, &new); Pubsub::pub_from_rename(tab, &old, &new);
let file = File::from(new.clone()).await?; let file = File::from(new.clone()).await?;
FilesOp::Deleting(p_old, vec![old]).emit(); if p_new == p_old {
FilesOp::Upserting(p_new, HashMap::from_iter([(new.clone(), file)])).emit(); 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)) Ok(TabProxy::reveal(&new))
} }

View file

@ -26,7 +26,7 @@ impl Manager {
let mut ops = vec![opt.op]; let mut ops = vec![opt.op];
for u in LINKED.read().from_dir(ops[0].cwd()) { 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 { for op in ops {
@ -60,10 +60,7 @@ impl Manager {
fn update_parent(tab: &mut Tab, op: Cow<FilesOp>) { fn update_parent(tab: &mut Tab, op: Cow<FilesOp>) {
let urn = tab.cwd().urn_owned(); let urn = tab.cwd().urn_owned();
// FIXME let leave = matches!(*op, FilesOp::Deleting(_, ref urns) if urns.contains(&urn));
let leave = false;
// let leave = matches!(*op, FilesOp::Deleting(_, ref urls) if
// urls.contains(&urn));
if let Some(f) = tab.parent.as_mut() { if let Some(f) = tab.parent.as_mut() {
render!(f.update(op.into_owned())); render!(f.update(op.into_owned()));
@ -111,8 +108,8 @@ impl Manager {
} }
fn update_history(tab: &mut Tab, op: Cow<FilesOp>) { 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( let leave = tab.parent.as_ref().and_then(|f| f.loc.parent_url().map(|p| (p, f.loc.urn()))).is_some_and(
|(p, pp)| matches!(*op, FilesOp::Deleting(ref parent, ref urls) if *parent == pp && urls.contains(p)), |(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())); let folder = tab.history.entry(op.cwd().clone()).or_insert_with(|| Folder::from(op.cwd()));

View file

@ -9,7 +9,7 @@ use tracing::error;
use yazi_fs::{Files, Folder}; use yazi_fs::{Files, Folder};
use yazi_plugin::isolate; use yazi_plugin::isolate;
use yazi_proxy::WATCHER; 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; use super::Linked;
@ -124,27 +124,26 @@ impl Watcher {
let _permit = WATCHER.acquire().await.unwrap(); let _permit = WATCHER.acquire().await.unwrap();
let mut reload = Vec::with_capacity(urls.len()); let mut reload = Vec::with_capacity(urls.len());
for url in urls { for u in urls {
let Some(name) = url.file_name() else { continue }; let Some((parent, urn)) = u.pair() else { continue };
let Some(parent) = url.parent_url() else { continue }; let Ok(file) = File::from(u).await else {
FilesOp::Deleting(parent, HashSet::from_iter([urn])).emit();
let Ok(file) = File::from(url.clone()).await else {
FilesOp::Deleting(parent, vec![url]).emit();
continue; continue;
}; };
let eq = (!file.is_link() && fs::canonicalize(&url).await.is_ok_and(|p| p == *url)) let u = file.url();
|| symlink_realname(&url, &mut cached).await.is_ok_and(|s| s == name); 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 { if !eq {
FilesOp::Deleting(parent, vec![url]).emit(); FilesOp::Deleting(parent, HashSet::from_iter([urn])).emit();
continue; continue;
} }
if !file.is_dir() { if !file.is_dir() {
reload.push(file.clone()); 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() { if reload.is_empty() {

View file

@ -39,17 +39,10 @@ impl Yanked {
} }
pub fn apply_op(&mut self, op: &FilesOp) { pub fn apply_op(&mut self, op: &FilesOp) {
let (removal, addition) = match op { let (removal, addition) = op.diff_recoverable(|u| self.contains(u));
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![]),
};
if !removal.is_empty() { if !removal.is_empty() {
let old = self.urls.len(); 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; self.revision += (old != self.urls.len()) as u64;
} }

View file

@ -18,23 +18,24 @@ impl Selected {
#[inline] #[inline]
pub fn add(&mut self, url: &Url) -> bool { self.add_same(&[url]) == 1 } 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 { if same {
return self.add_same(urls); return self.add_same(urls);
} }
let mut grouped: HashMap<_, Vec<_>> = Default::default(); let mut grouped: HashMap<_, Vec<_>> = Default::default();
for &u in urls { for u in urls {
if let Some(p) = u.parent_url() { if let Some(p) = u.as_ref().parent_url() {
grouped.entry(p).or_default().push(u); grouped.entry(p).or_default().push(u);
} }
} }
grouped.into_values().map(|v| self.add_same(&v)).sum() 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 // 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() { if urls.is_empty() {
return 0; return 0;
} }
@ -52,7 +53,7 @@ impl Selected {
} }
let (now, len) = (timestamp_us(), self.inner.len()); 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 { for u in parents {
*self.parents.entry(u).or_insert(0) += self.inner.len() - len; *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) { pub fn apply_op(&mut self, op: &FilesOp) {
let (removal, addition) = match op { let (removal, addition) = op.diff_recoverable(|u| self.contains_key(u));
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![]),
};
if !removal.is_empty() { if !removal.is_empty() {
self.remove_many(&removal, !op.cwd().is_search()); self.remove_many(&removal, !op.cwd().is_search());
} }
@ -196,7 +190,7 @@ mod tests {
fn insert_many_empty_urls_list() { fn insert_many_empty_urls_list() {
let mut s = Selected::default(); let mut s = Selected::default();
assert_eq!(0, s.add_same(&[])); assert_eq!(0, s.add_same(&[] as &[&Url]));
} }
#[test] #[test]

View file

@ -72,7 +72,7 @@ impl Tasks {
targets targets
.iter() .iter()
.filter(|f| { .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()) .map(|f| f.url())
.collect() .collect()

View file

@ -39,7 +39,7 @@ impl File {
reg.add_field_method_get("idx", |_, me| Ok(me.idx + 1)); reg.add_field_method_get("idx", |_, me| Ok(me.idx + 1));
reg.add_method("size", |_, me, ()| { 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, ()| { reg.add_method("mime", |lua, me, ()| {
let cx = lua.named_registry_value::<CtxRef>("cx")?; let cx = lua.named_registry_value::<CtxRef>("cx")?;

View file

@ -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 tokio::{fs::{self, DirEntry}, select, sync::mpsc::{self, UnboundedReceiver}};
use yazi_config::{manager::SortBy, MANAGER}; 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}; use super::{FilesSorter, Filter};
@ -13,7 +13,7 @@ pub struct Files {
version: u64, version: u64,
pub revision: u64, pub revision: u64,
pub sizes: HashMap<Url, u64>, pub sizes: HashMap<UrnBuf, u64>,
sorter: FilesSorter, sorter: FilesSorter,
filter: Option<Filter>, filter: Option<Filter>,
@ -99,7 +99,7 @@ impl Files {
pub async fn assert_stale(cwd: &Url, cha: Cha) -> Option<Cha> { pub async fn assert_stale(cwd: &Url, cha: Cha) -> Option<Cha> {
match fs::metadata(cwd).await.map(Cha::from) { match fs::metadata(cwd).await.map(Cha::from) {
Ok(c) if !c.is_dir() => { 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(); FilesOp::IOErr(cwd.clone(), std::io::ErrorKind::AlreadyExists).emit();
} }
Ok(c) if c.hits(cha) => {} Ok(c) if c.hits(cha) => {}
@ -107,8 +107,8 @@ impl Files {
Err(e) => { Err(e) => {
if maybe_exists(cwd).await { if maybe_exists(cwd).await {
FilesOp::IOErr(cwd.clone(), e.kind()).emit(); FilesOp::IOErr(cwd.clone(), e.kind()).emit();
} else if let Some(p) = cwd.parent_url() { } else if let Some((p, n)) = cwd.pair() {
FilesOp::Deleting(p, vec![cwd.clone()]).emit(); 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() { if sizes.is_empty() {
return; return;
} }
@ -197,31 +197,30 @@ impl Files {
} }
#[cfg(unix)] #[cfg(unix)]
pub fn update_deleting(&mut self, urls: Vec<Url>) { pub fn update_deleting(&mut self, urns: HashSet<UrnBuf>) {
if urls.is_empty() { if urns.is_empty() {
return; return;
} }
macro_rules! go { macro_rules! go {
($dist:expr, $src:expr, $inc:literal) => { ($dist:expr, $src:expr, $inc:literal) => {
let mut todo: HashSet<_> = $src.into_iter().collect();
let len = $dist.len(); let len = $dist.len();
$dist.retain(|f| !$src.remove(f.urn()));
$dist.retain(|f| !todo.remove(f.url()));
if $dist.len() != len { if $dist.len() != len {
self.revision += $inc; self.revision += $inc;
} }
}; };
} }
let (hidden, items) = if let Some(filter) = &self.filter { let (mut hidden, mut items) = if let Some(filter) = &self.filter {
urls.into_iter().partition(|u| { urns.into_iter().partition(|u| {
(!self.show_hidden && u.is_hidden()) || !u.file_name().is_some_and(|s| filter.matches(s)) (!self.show_hidden && u._deref().is_hidden())
|| !u._deref().name().is_some_and(|s| filter.matches(s))
}) })
} else if self.show_hidden { } else if self.show_hidden {
(vec![], urls) (HashSet::new(), urns)
} else { } else {
urls.into_iter().partition(|u| u.is_hidden()) urns.into_iter().partition(|u| u._deref().is_hidden())
}; };
if !items.is_empty() { if !items.is_empty() {
@ -233,31 +232,29 @@ impl Files {
} }
#[cfg(windows)] #[cfg(windows)]
pub fn update_deleting(&mut self, urls: Vec<Url>) { pub fn update_deleting(&mut self, mut urns: HashSet<UrnBuf>) {
macro_rules! go { macro_rules! go {
($dist:expr, $src:expr, $inc:literal) => { ($dist:expr, $src:expr, $inc:literal) => {
let len = $dist.len(); let len = $dist.len();
$dist.retain(|f| !$src.remove(f.urn()));
$dist.retain(|f| !$src.remove(f.url()));
if $dist.len() != len { if $dist.len() != len {
self.revision += $inc; self.revision += $inc;
} }
}; };
} }
let mut urls: HashSet<_> = urls.into_iter().collect(); if !urns.is_empty() {
if !urls.is_empty() { go!(self.items, urns, 1);
go!(self.items, urls, 1);
} }
if !urls.is_empty() { if !urns.is_empty() {
go!(self.hidden, urls, 0); go!(self.hidden, urns, 0);
} }
} }
pub fn update_updating( pub fn update_updating(
&mut self, &mut self,
files: HashMap<Url, File>, files: HashMap<UrnBuf, File>,
) -> (HashMap<Url, File>, HashMap<Url, File>) { ) -> (HashMap<UrnBuf, File>, HashMap<UrnBuf, File>) {
if files.is_empty() { if files.is_empty() {
return Default::default(); return Default::default();
} }
@ -266,7 +263,7 @@ impl Files {
($dist:expr, $src:expr, $inc:literal) => { ($dist:expr, $src:expr, $inc:literal) => {
let mut b = true; let mut b = true;
for i in 0..$dist.len() { 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); b &= $dist[i].cha.hits(f.cha);
$dist[i] = f; $dist[i] = f;
@ -298,7 +295,7 @@ impl Files {
(hidden, items) (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() { if files.is_empty() {
return; return;
} }

View file

@ -52,7 +52,7 @@ impl Folder {
FilesOp::IOErr(..) => self.files.update_ioerr(), FilesOp::IOErr(..) => self.files.update_ioerr(),
FilesOp::Creating(_, files) => self.files.update_creating(files), 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::Updating(_, files) => _ = self.files.update_updating(files),
FilesOp::Upserting(_, files) => self.files.update_upserting(files), FilesOp::Upserting(_, files) => self.files.update_upserting(files),
} }

View file

@ -1,7 +1,7 @@
use std::{cmp::Ordering, collections::HashMap, mem}; use std::{cmp::Ordering, collections::HashMap, mem};
use yazi_config::manager::SortBy; 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)] #[derive(Clone, Copy, Default, PartialEq)]
pub struct FilesSorter { pub struct FilesSorter {
@ -13,7 +13,7 @@ pub struct FilesSorter {
} }
impl 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() { if items.is_empty() {
return; return;
} }
@ -51,8 +51,8 @@ impl FilesSorter {
SortBy::Alphabetical => items.sort_unstable_by(by_alphabetical), SortBy::Alphabetical => items.sort_unstable_by(by_alphabetical),
SortBy::Natural => self.sort_naturally(items), SortBy::Natural => self.sort_naturally(items),
SortBy::Size => items.sort_unstable_by(|a, b| { SortBy::Size => items.sort_unstable_by(|a, b| {
let aa = if a.is_dir() { sizes.get(a.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.url()).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)); 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 } if ord == Ordering::Equal { by_alphabetical(a, b) } else { ord }
}), }),

View file

@ -111,7 +111,8 @@ pub fn install(lua: &Lua) -> mlua::Result<()> {
( (
"unique_name", "unique_name",
lua.create_async_function(|lua, url: UrlRef| async move { 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) Url::cast(lua, yazi_shared::fs::unique_name(url.clone()).await)
})?, })?,
), ),

View file

@ -92,7 +92,8 @@ impl Prework {
} }
let parent = buf[0].0.parent_url().unwrap(); 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))?; self.prog.send(TaskProg::Adv(task.id, 1, 0))?;
} }

View file

@ -81,7 +81,7 @@ impl File {
} }
#[cfg(unix)] #[cfg(unix)]
if loc.url().is_hidden() { if loc.urn().is_hidden() {
ck |= ChaKind::HIDDEN; ck |= ChaKind::HIDDEN;
} }
#[cfg(windows)] #[cfg(windows)]

View file

@ -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 anyhow::{bail, Result};
use tokio::{fs, io, select, sync::{mpsc, oneshot}, time}; 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?) Ok(final_name(a).await? == final_name(b).await?)
} }
pub async fn symlink_realpath(path: &Path) -> Result<PathBuf> { pub async fn realname(p: &Path) -> Option<OsString> {
let p = fs::canonicalize(path).await?; let name = p.file_name()?;
if p == path { if p == fs::canonicalize(p).await.ok()? {
return Ok(p); return None;
} }
let Some(parent) = path.parent() else { bail!("no parent") }; realname_unchecked(p, &mut HashMap::new())
symlink_realname(path, &mut HashMap::new()).await.map(|n| parent.join(n)) .await
.ok()
.filter(|s| s != name)
.map(|s| s.into_owned())
} }
#[cfg(unix)] #[cfg(unix)]
#[tokio::test] #[tokio::test]
async fn test_symlink_realpath() { async fn test_realname_unchecked() {
fs::remove_dir_all("/tmp/issue-1173").await.ok(); fs::remove_dir_all("/tmp/issue-1173").await.ok();
fs::create_dir_all("/tmp/issue-1173/real-dir").await.unwrap(); fs::create_dir_all("/tmp/issue-1173/real-dir").await.unwrap();
fs::File::create("/tmp/issue-1173/A").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("/tmp/issue-1173/b", "/tmp/issue-1173/D").await.unwrap();
fs::symlink("real-dir", "/tmp/issue-1173/link-dir").await.unwrap(); fs::symlink("real-dir", "/tmp/issue-1173/link-dir").await.unwrap();
async fn check(a: &str, b: &str) { let c = &mut HashMap::new();
let expected = if a == b || cfg!(windows) || cfg!(target_os = "macos") { async fn check(a: &str, b: &str, c: &mut HashMap<PathBuf, HashSet<OsString>>) {
Some(PathBuf::from(b)) assert_eq!(realname_unchecked(Path::new(a), c).await.ok(), Some(OsStr::new(b).into()));
} else {
None
};
assert_eq!(symlink_realpath(Path::new(a)).await.ok(), expected);
} }
check("/tmp/issue-1173/a", "/tmp/issue-1173/A").await; check("/tmp/issue-1173/a", "A", c).await;
check("/tmp/issue-1173/A", "/tmp/issue-1173/A").await; check("/tmp/issue-1173/A", "A", c).await;
check("/tmp/issue-1173/b", "/tmp/issue-1173/b").await; check("/tmp/issue-1173/b", "b", c).await;
check("/tmp/issue-1173/B", "/tmp/issue-1173/b").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", "C", 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/d", "/tmp/issue-1173/D").await; check("/tmp/issue-1173/d", "D", c).await;
check("/tmp/issue-1173/D", "/tmp/issue-1173/D").await; check("/tmp/issue-1173/D", "D", c).await;
} }
// realpath(3) without resolving symlinks. This is useful for case-insensitive // realpath(3) without resolving symlinks. This is useful for case-insensitive
// filesystems. // filesystems.
// //
// Make sure the file of the path exists. // Make sure the file of the path exists.
pub async fn symlink_realname<'a>( pub async fn realname_unchecked<'a>(
path: &'a Path, path: &'a Path,
cached: &'a mut HashMap<PathBuf, HashMap<OsString, OsString>>, cached: &'a mut HashMap<PathBuf, HashSet<OsString>>,
) -> Result<Cow<'a, OsStr>> { ) -> Result<Cow<'a, OsStr>> {
let Some(name) = path.file_name() else { bail!("no file name") }; let Some(name) = path.file_name() else { bail!("no filename") };
let Some(parent) = path.parent() else { return Ok(name.into()) }; let Some(parent) = path.parent() else { return Ok(Cow::Borrowed(name)) };
if !cached.contains_key(parent) { if !cached.contains_key(parent) {
let mut map = HashMap::new(); let mut set = HashSet::new();
let mut it = fs::read_dir(parent).await?; let mut it = fs::read_dir(parent).await?;
while let Some(entry) = it.next_entry().await? { while let Some(entry) = it.next_entry().await? {
let n = entry.file_name(); set.insert(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);
}
} }
cached.insert(parent.to_owned(), map); cached.insert(parent.to_owned(), set);
} }
let c = &cached[parent]; let c = &cached[parent];
if let Some(s) = c.get(name) { if c.contains(name) {
return if s.is_empty() { Ok(name.into()) } else { Ok(s.into()) }; 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 { pub async fn calculate_size(path: &Path) -> u64 {

View file

@ -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}; use crate::{emit, event::Cmd, fs::Url, Layer};
pub static FILES_TICKET: AtomicU64 = AtomicU64::new(0); pub static FILES_TICKET: AtomicU64 = AtomicU64::new(0);
@ -10,13 +10,13 @@ pub enum FilesOp {
Full(Url, Vec<File>, Cha), Full(Url, Vec<File>, Cha),
Part(Url, Vec<File>, u64), Part(Url, Vec<File>, u64),
Done(Url, Cha, u64), Done(Url, Cha, u64),
Size(Url, HashMap<Url, u64>), Size(Url, HashMap<UrnBuf, u64>),
IOErr(Url, std::io::ErrorKind), IOErr(Url, std::io::ErrorKind),
Creating(Url, Vec<File>), Creating(Url, Vec<File>),
Deleting(Url, Vec<Url>), Deleting(Url, HashSet<UrnBuf>),
Updating(Url, HashMap<Url, File>), Updating(Url, HashMap<UrnBuf, File>),
Upserting(Url, HashMap<Url, File>), Upserting(Url, HashMap<UrnBuf, File>),
} }
impl FilesOp { impl FilesOp {
@ -47,15 +47,12 @@ impl FilesOp {
ticket ticket
} }
pub fn chroot(&self, new: &Url) -> Self { pub fn rebase(&self, new: &Url) -> Self {
macro_rules! new {
($url:expr) => {{ new.join($url.file_name().unwrap()) }};
}
macro_rules! files { macro_rules! files {
($files:expr) => {{ $files.iter().map(|f| f.rebase(new)).collect() }}; ($files:expr) => {{ $files.iter().map(|f| f.rebase(new)).collect() }};
} }
macro_rules! map { 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(); let n = new.clone();
@ -63,13 +60,29 @@ impl FilesOp {
Self::Full(_, files, mtime) => Self::Full(n, files!(files), *mtime), Self::Full(_, files, mtime) => Self::Full(n, files!(files), *mtime),
Self::Part(_, files, ticket) => Self::Part(n, files!(files), *ticket), Self::Part(_, files, ticket) => Self::Part(n, files!(files), *ticket),
Self::Done(_, mtime, ticket) => Self::Done(n, *mtime, *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::IOErr(_, err) => Self::IOErr(n, *err),
Self::Creating(_, files) => Self::Creating(n, files!(files)), 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::Updating(_, map) => Self::Updating(n, map!(map)),
Self::Upserting(_, map) => Self::Upserting(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![]),
}
}
} }

View file

@ -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 { pub async fn unique_name(mut u: Url) -> Url {
let Some(stem) = u.file_stem().map(|s| s.to_owned()) else { let Some(stem) = u.file_stem().map(|s| s.to_owned()) else {
return u; return u;

View file

@ -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 percent_encoding::{percent_decode_str, percent_encode, AsciiSet, CONTROLS};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::Loc; use super::{Loc, UrnBuf};
const ENCODE_SET: &AsciiSet = &CONTROLS.add(b'#'); 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] #[inline]
pub fn strip_prefix(&self, base: impl AsRef<Path>) -> Option<&Path> { pub fn strip_prefix(&self, base: impl AsRef<Path>) -> Option<&Path> {
self.path.strip_prefix(base).ok() self.path.strip_prefix(base).ok()
@ -158,12 +164,6 @@ impl Url {
#[inline] #[inline]
pub fn into_os_string(self) -> OsString { self.path.into_os_string() } 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] #[inline]
pub fn to_loc(&self, cwd: &Url) -> Loc { self.clone().into_loc(cwd) } pub fn to_loc(&self, cwd: &Url) -> Loc { self.clone().into_loc(cwd) }

View file

@ -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)] #[repr(transparent)]
pub struct Urn(Path); pub struct Urn(Path);
@ -10,7 +10,16 @@ impl Urn {
unsafe { &*(p.as_ref() as *const Path as *const Self) } 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 } pub fn _as_path(&self) -> &Path { &self.0 }
} }
@ -21,13 +30,21 @@ impl ToOwned for Urn {
} }
// --- UrnBuf // --- UrnBuf
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct UrnBuf(PathBuf); pub struct UrnBuf(PathBuf);
impl Borrow<Urn> for UrnBuf { impl Borrow<Urn> for UrnBuf {
fn borrow(&self) -> &Urn { Urn::new(&self.0) } fn borrow(&self) -> &Urn { Urn::new(&self.0) }
} }
impl UrnBuf { impl PartialEq<Urn> for UrnBuf {
// FIXME: remove this fn eq(&self, other: &Urn) -> bool { self.0 == other.0 }
pub fn _deref(&self) -> &Urn { Urn::new(&self.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()) }
} }