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_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);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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")?;

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

View file

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

View file

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

View file

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

View file

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

View file

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

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};
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![]),
}
}
}

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 {
let Some(stem) = u.file_stem().map(|s| s.to_owned()) else {
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 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) }

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