From b303219b99c935050058f6b3123188285c4e50fd Mon Sep 17 00:00:00 2001 From: sxyazi Date: Wed, 12 Jun 2024 18:12:36 +0800 Subject: [PATCH 1/3] fix: different filenames should be treated as the same file on case-insensitive file systems --- Cargo.lock | 28 ++++++------- cspell.json | 2 +- yazi-boot/Cargo.toml | 12 +++--- yazi-cli/Cargo.toml | 10 ++--- yazi-core/Cargo.toml | 2 +- yazi-core/src/manager/commands/create.rs | 48 ++++++++++++++--------- yazi-core/src/manager/commands/rename.rs | 16 ++++---- yazi-core/src/manager/watcher.rs | 38 ++++++++++++------ yazi-dds/src/body/body.rs | 12 ++++-- yazi-scheduler/src/file/file.rs | 43 ++++++++------------ yazi-shared/Cargo.toml | 2 +- yazi-shared/src/fs/fns.rs | 50 +++++++++++++++++++++++- 12 files changed, 162 insertions(+), 101 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1afd63ed4..c34506345 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -293,9 +293,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", @@ -303,9 +303,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", @@ -315,18 +315,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.2" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e" +checksum = "d2020fa13af48afc65a9a87335bda648309ab3d154cd03c7ff95b378c7ed39c4" dependencies = [ "clap", ] [[package]] name = "clap_complete_fig" -version = "4.5.0" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b3e65f91fabdd23cac3d57d39d5d938b4daabd070c335c006dccb866a61110" +checksum = "fb4bc503cddc1cd320736fb555d6598309ad07c2ddeaa23891a10ffb759ee612" dependencies = [ "clap", "clap_complete", @@ -334,9 +334,9 @@ dependencies = [ [[package]] name = "clap_complete_nushell" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0e48e026ce7df2040239117d25e4e79714907420c70294a5ce4b6bbe6a7b6" +checksum = "1accf1b463dee0d3ab2be72591dccdab8bef314958340447c882c4c72acfe2a3" dependencies = [ "clap", "clap_complete", @@ -344,9 +344,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1617,9 +1617,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", diff --git a/cspell.json b/cspell.json index db858584d..c7b1bfd32 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"version":"0.2","words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","imagesize","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt"],"language":"en","flagWords":[]} \ No newline at end of file +{"language":"en","version":"0.2","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","imagesize","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath"]} \ No newline at end of file diff --git a/yazi-boot/Cargo.toml b/yazi-boot/Cargo.toml index 15ef21ed6..51d4a6941 100644 --- a/yazi-boot/Cargo.toml +++ b/yazi-boot/Cargo.toml @@ -9,18 +9,18 @@ homepage = "https://yazi-rs.github.io" repository = "https://github.com/sxyazi/yazi" [dependencies] -regex = "1.10.4" +regex = "1.10.5" yazi-adapter = { path = "../yazi-adapter", version = "0.2.5" } yazi-config = { path = "../yazi-config", version = "0.2.5" } yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies -clap = { version = "4.5.4", features = [ "derive" ] } +clap = { version = "4.5.7", features = [ "derive" ] } serde = { version = "1.0.203", features = [ "derive" ] } [build-dependencies] -clap = { version = "4.5.4", features = [ "derive" ] } -clap_complete = "4.5.2" -clap_complete_nushell = "4.5.1" -clap_complete_fig = "4.5.0" +clap = { version = "4.5.7", features = [ "derive" ] } +clap_complete = "4.5.5" +clap_complete_nushell = "4.5.2" +clap_complete_fig = "4.5.1" vergen = { version = "8.3.1", features = [ "build", "git", "gitcl" ] } diff --git a/yazi-cli/Cargo.toml b/yazi-cli/Cargo.toml index 74572e437..4b706ed07 100644 --- a/yazi-cli/Cargo.toml +++ b/yazi-cli/Cargo.toml @@ -14,7 +14,7 @@ yazi-shared = { path = "../yazi-shared", version = "0.2.5" } # External dependencies anyhow = "1.0.86" -clap = { version = "4.5.4", features = [ "derive" ] } +clap = { version = "4.5.7", features = [ "derive" ] } crossterm = "0.27.0" md-5 = "0.10.6" serde_json = "1.0.117" @@ -23,10 +23,10 @@ toml_edit = "0.22.14" [build-dependencies] anyhow = "1.0.86" -clap = { version = "4.5.4", features = [ "derive" ] } -clap_complete = "4.5.2" -clap_complete_fig = "4.5.0" -clap_complete_nushell = "4.5.1" +clap = { version = "4.5.7", features = [ "derive" ] } +clap_complete = "4.5.5" +clap_complete_fig = "4.5.1" +clap_complete_nushell = "4.5.2" serde_json = "1.0.117" vergen = { version = "8.3.1", features = [ "build", "git", "gitcl" ] } diff --git a/yazi-core/Cargo.toml b/yazi-core/Cargo.toml index 7cd656692..50442c8b3 100644 --- a/yazi-core/Cargo.toml +++ b/yazi-core/Cargo.toml @@ -27,7 +27,7 @@ futures = "0.3.30" notify = { version = "6.1.1", default-features = false, features = [ "macos_fsevent" ] } parking_lot = "0.12.3" ratatui = "0.26.3" -regex = "1.10.4" +regex = "1.10.5" scopeguard = "1.2.0" serde = "1.0.203" shell-words = "1.1.0" diff --git a/yazi-core/src/manager/commands/create.rs b/yazi-core/src/manager/commands/create.rs index 2f3c4a014..55554b9c3 100644 --- a/yazi-core/src/manager/commands/create.rs +++ b/yazi-core/src/manager/commands/create.rs @@ -1,9 +1,10 @@ -use std::path::PathBuf; +use std::collections::HashMap; +use anyhow::Result; use tokio::fs; use yazi_config::popup::InputCfg; -use yazi_proxy::{InputProxy, ManagerProxy}; -use yazi_shared::{event::Cmd, fs::{maybe_exists, File, FilesOp, Url}}; +use yazi_proxy::{InputProxy, TabProxy, WATCHER}; +use yazi_shared::{event::Cmd, fs::{maybe_exists, ok_or_not_found, File, FilesOp, Url}}; use crate::manager::Manager; @@ -24,29 +25,38 @@ impl Manager { let Some(Ok(name)) = result.recv().await else { return Ok(()); }; + if name.is_empty() { + return Ok(()); + } - let path = cwd.join(&name); - if !opt.force && maybe_exists(&path).await { + let new = cwd.join(&name); + if !opt.force && maybe_exists(&new).await { match InputProxy::show(InputCfg::overwrite()).recv().await { Some(Ok(c)) if c == "y" || c == "Y" => (), _ => return Ok(()), } } - if name.ends_with('/') || name.ends_with('\\') { - fs::create_dir_all(&path).await?; - } else { - fs::create_dir_all(&path.parent().unwrap()).await.ok(); - fs::File::create(&path).await?; - } - - let child = - Url::from(path.components().take(cwd.components().count() + 1).collect::()); - if let Ok(f) = File::from(child.clone()).await { - FilesOp::Creating(cwd, vec![f]).emit(); - ManagerProxy::hover(Some(child)); - } - Ok::<(), anyhow::Error>(()) + Self::create_do(new, name.ends_with('/') || name.ends_with('\\')).await }); } + + async fn create_do(new: Url, dir: bool) -> Result<()> { + let Some(parent) = new.parent_url() else { return Ok(()) }; + let _permit = WATCHER.acquire().await.unwrap(); + + if dir { + fs::create_dir_all(&new).await?; + } else { + fs::create_dir_all(&parent).await.ok(); + ok_or_not_found(fs::remove_file(&new).await)?; + fs::File::create(&new).await?; + } + + if let Ok(f) = File::from(new.clone()).await { + FilesOp::Upserting(parent, HashMap::from_iter([(new.clone(), f)])).emit(); + TabProxy::reveal(&new) + } + Ok(()) + } } diff --git a/yazi-core/src/manager/commands/rename.rs b/yazi-core/src/manager/commands/rename.rs index 3892060b9..70b9e3718 100644 --- a/yazi-core/src/manager/commands/rename.rs +++ b/yazi-core/src/manager/commands/rename.rs @@ -4,7 +4,7 @@ use anyhow::Result; use tokio::fs; use yazi_config::popup::InputCfg; use yazi_dds::Pubsub; -use yazi_proxy::{InputProxy, ManagerProxy, WATCHER}; +use yazi_proxy::{InputProxy, TabProxy, WATCHER}; use yazi_shared::{event::Cmd, fs::{maybe_exists, File, FilesOp, Url}}; use crate::manager::Manager; @@ -77,19 +77,17 @@ 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 _permit = WATCHER.acquire().await.unwrap(); fs::rename(&old, &new).await?; - if old.parent() != new.parent() { - return Ok(()); - } - - let file = File::from(new.clone()).await?; Pubsub::pub_from_rename(tab, &old, &new); - FilesOp::Deleting(file.parent().unwrap(), vec![new.clone()]).emit(); - FilesOp::Upserting(file.parent().unwrap(), HashMap::from_iter([(old, file)])).emit(); - Ok(ManagerProxy::hover(Some(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(); + Ok(TabProxy::reveal(&new)) } fn empty_url_part(url: &Url, by: &str) -> String { diff --git a/yazi-core/src/manager/watcher.rs b/yazi-core/src/manager/watcher.rs index 7a2632b13..45cef5cb5 100644 --- a/yazi-core/src/manager/watcher.rs +++ b/yazi-core/src/manager/watcher.rs @@ -1,4 +1,4 @@ -use std::{collections::{HashMap, HashSet}, time::{Duration, SystemTime}}; +use std::{borrow::Cow, collections::{HashMap, HashSet}, time::{Duration, SystemTime}}; use anyhow::Result; use notify::{RecommendedWatcher, RecursiveMode, Watcher as _Watcher}; @@ -8,7 +8,7 @@ use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; use tracing::error; use yazi_plugin::isolate; use yazi_proxy::WATCHER; -use yazi_shared::{fs::{File, FilesOp, Url}, RoCell}; +use yazi_shared::{fs::{symlink_realpath, File, FilesOp, Url}, RoCell}; use super::Linked; use crate::folder::{Files, Folder}; @@ -35,8 +35,8 @@ impl Watcher { Default::default(), ); - tokio::spawn(Self::on_in(in_rx, watcher.unwrap())); - tokio::spawn(Self::on_out(out_rx)); + tokio::spawn(Self::fan_in(in_rx, watcher.unwrap())); + tokio::spawn(Self::fan_out(out_rx)); Self { tx: in_tx } } @@ -65,7 +65,7 @@ impl Watcher { }); } - async fn on_in(mut rx: watch::Receiver>, mut watcher: RecommendedWatcher) { + async fn fan_in(mut rx: watch::Receiver>, mut watcher: RecommendedWatcher) { loop { let (mut to_unwatch, mut to_watch): (HashSet<_>, HashSet<_>) = { let (new, old) = (&*rx.borrow_and_update(), &*WATCHED.read()); @@ -91,27 +91,39 @@ impl Watcher { } } - async fn on_out(rx: UnboundedReceiver) { + async fn fan_out(rx: UnboundedReceiver) { // TODO: revert this once a new notification is implemented let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(50)); pin!(rx); - while let Some(urls) = rx.next().await { + while let Some(chunk) = rx.next().await { + let urls: HashSet<_> = chunk.into_iter().collect(); + let mut cached: HashMap<_, _> = HashMap::new(); + let _permit = WATCHER.acquire().await.unwrap(); let mut reload = Vec::with_capacity(urls.len()); - for u in urls.into_iter().collect::>() { - let Some(parent) = u.parent_url() else { continue }; - - let Ok(file) = File::from(u.clone()).await else { - FilesOp::Deleting(parent, vec![u]).emit(); + for url in urls { + let Some(parent) = url.parent_url() else { continue }; + let Ok(file) = File::from(url.clone()).await else { + FilesOp::Deleting(parent, vec![url]).emit(); continue; }; + let realpath = if file.is_link() { + symlink_realpath(&url, &mut cached).await + } else { + fs::canonicalize(&url).await.map(Cow::Owned) + }; + if !realpath.is_ok_and(|p| p == *url) { + FilesOp::Deleting(parent, vec![url]).emit(); + continue; + } + if !file.is_dir() { reload.push(file.clone()); } - FilesOp::Upserting(parent, HashMap::from_iter([(u, file)])).emit(); + FilesOp::Upserting(parent, HashMap::from_iter([(url, file)])).emit(); } if reload.is_empty() { diff --git a/yazi-dds/src/body/body.rs b/yazi-dds/src/body/body.rs index ee8125219..1f82b85ea 100644 --- a/yazi-dds/src/body/body.rs +++ b/yazi-dds/src/body/body.rs @@ -63,11 +63,15 @@ impl Body<'static> { if matches!( kind, "hi" - | "hey" | "bye" - | "cd" | "hover" + | "hey" + | "bye" + | "cd" + | "hover" | "rename" - | "bulk" | "yank" - | "move" | "trash" + | "bulk" + | "yank" + | "move" + | "trash" | "delete" ) { bail!("Cannot construct system event"); diff --git a/yazi-scheduler/src/file/file.rs b/yazi-scheduler/src/file/file.rs index 19189ebf9..a68f0ac06 100644 --- a/yazi-scheduler/src/file/file.rs +++ b/yazi-scheduler/src/file/file.rs @@ -5,7 +5,7 @@ use futures::{future::BoxFuture, FutureExt}; use tokio::{fs, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; use tracing::warn; use yazi_config::TASKS; -use yazi_shared::fs::{calculate_size, copy_with_progress, maybe_exists, path_relative_to, Url}; +use yazi_shared::fs::{calculate_size, copy_with_progress, maybe_exists, ok_or_not_found, path_relative_to, Url}; use super::{FileOp, FileOpDelete, FileOpLink, FileOpPaste, FileOpTrash}; use crate::{TaskOp, TaskProg, LOW, NORMAL}; @@ -26,12 +26,9 @@ impl File { pub async fn work(&self, op: FileOp) -> Result<()> { match op { FileOp::Paste(mut task) => { - match fs::remove_file(&task.to).await { - Err(e) if e.kind() != NotFound => Err(e)?, - _ => {} - } - + ok_or_not_found(fs::remove_file(&task.to).await)?; let mut it = copy_with_progress(&task.from, &task.to, task.meta.as_ref().unwrap()); + while let Some(res) = it.recv().await { match res { Ok(0) => { @@ -83,21 +80,17 @@ impl File { src }; - match fs::remove_file(&task.to).await { - Err(e) if e.kind() != NotFound => Err(e)?, - _ => { - #[cfg(unix)] - { - fs::symlink(src, &task.to).await? - } - #[cfg(windows)] - { - if meta.is_dir() { - fs::symlink_dir(src, &task.to).await? - } else { - fs::symlink_file(src, &task.to).await? - } - } + ok_or_not_found(fs::remove_file(&task.to).await)?; + #[cfg(unix)] + { + fs::symlink(src, &task.to).await? + } + #[cfg(windows)] + { + if meta.is_dir() { + fs::symlink_dir(src, &task.to).await? + } else { + fs::symlink_file(src, &task.to).await? } } @@ -134,12 +127,8 @@ impl File { } pub async fn paste(&self, mut task: FileOpPaste) -> Result<()> { - if task.cut { - match fs::rename(&task.from, &task.to).await { - Ok(_) => return self.succ(task.id), - Err(e) if e.kind() == NotFound => return self.succ(task.id), - _ => {} - } + if task.cut && ok_or_not_found(fs::rename(&task.from, &task.to).await).is_ok() { + return self.succ(task.id); } if task.meta.is_none() { diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index ed3391f69..2d8c7e9e6 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -19,7 +19,7 @@ futures = "0.3.30" parking_lot = "0.12.3" percent-encoding = "2.3.1" ratatui = "0.26.3" -regex = "1.10.4" +regex = "1.10.5" serde = { version = "1.0.203", features = [ "derive" ] } shell-words = "1.1.0" tokio = { version = "1.38.0", features = [ "full" ] } diff --git a/yazi-shared/src/fs/fns.rs b/yazi-shared/src/fs/fns.rs index 186f9b338..ea727d0e1 100644 --- a/yazi-shared/src/fs/fns.rs +++ b/yazi-shared/src/fs/fns.rs @@ -1,11 +1,13 @@ -use std::{collections::VecDeque, fs::Metadata, path::{Path, PathBuf}}; +use std::{borrow::Cow, collections::{HashMap, VecDeque}, fs::Metadata, path::{Path, PathBuf}}; use anyhow::Result; use filetime::{set_file_mtime, FileTime}; use tokio::{fs, io, select, sync::{mpsc, oneshot}, time}; +#[inline] pub async fn must_exists(p: impl AsRef) -> bool { fs::symlink_metadata(p).await.is_ok() } +#[inline] pub async fn maybe_exists(p: impl AsRef) -> bool { match fs::symlink_metadata(p).await { Ok(_) => true, @@ -13,6 +15,52 @@ pub async fn maybe_exists(p: impl AsRef) -> bool { } } +#[inline] +pub fn ok_or_not_found(result: io::Result<()>) -> io::Result<()> { + match result { + Ok(()) => Ok(()), + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()), + Err(_) => result, + } +} + +// realpath(3) without resolving symlinks. This is useful for case-insensitive +// filesystems. +// +// Make sure the file of the path exists and is a symlink. +pub async fn symlink_realpath<'a>( + path: &'a Path, + cached: &'a mut HashMap, +) -> io::Result> { + let Some(parent) = path.parent() else { + return Ok(Cow::Borrowed(path)); + }; + + let lowercased: PathBuf = path.as_os_str().to_ascii_lowercase().into(); + if lowercased == path { + return Ok(Cow::Borrowed(path)); + } + + let case = parent.as_os_str().as_encoded_bytes().iter().any(|&b| b.is_ascii_uppercase()); + if !cached.contains_key(parent) { + let mut it = fs::read_dir(parent).await?; + while let Some(entry) = it.next_entry().await? { + let p = entry.path(); + if case || p.file_name().unwrap().as_encoded_bytes().iter().any(|&b| b.is_ascii_uppercase()) { + cached.insert(p.as_os_str().to_ascii_lowercase().into(), p); + } + } + cached.insert(parent.to_owned(), PathBuf::new()); + } + + Ok( + cached + .get(&lowercased) + .filter(|p| !p.as_os_str().is_empty()) + .map_or_else(|| Cow::Borrowed(path), |p| Cow::Borrowed(p)), + ) +} + pub async fn calculate_size(path: &Path) -> u64 { let mut total = 0; let mut stack = VecDeque::from([path.to_path_buf()]); From 8eb2eea5cc4e449c83ae3a601862b402efa2eaeb Mon Sep 17 00:00:00 2001 From: sxyazi Date: Thu, 13 Jun 2024 20:28:44 +0800 Subject: [PATCH 2/3] .. --- yazi-core/src/manager/commands/create.rs | 8 ++++++-- yazi-core/src/manager/commands/rename.rs | 8 +++++++- yazi-core/src/manager/watcher.rs | 4 ++-- yazi-shared/src/fs/fns.rs | 19 ++++++++++++++----- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/yazi-core/src/manager/commands/create.rs b/yazi-core/src/manager/commands/create.rs index 55554b9c3..351b40b5d 100644 --- a/yazi-core/src/manager/commands/create.rs +++ b/yazi-core/src/manager/commands/create.rs @@ -4,7 +4,7 @@ use anyhow::Result; use tokio::fs; use yazi_config::popup::InputCfg; use yazi_proxy::{InputProxy, TabProxy, WATCHER}; -use yazi_shared::{event::Cmd, fs::{maybe_exists, ok_or_not_found, File, FilesOp, Url}}; +use yazi_shared::{event::Cmd, fs::{maybe_exists, ok_or_not_found, symlink_realpath, File, FilesOp, Url}}; use crate::manager::Manager; @@ -47,6 +47,10 @@ impl Manager { if dir { fs::create_dir_all(&new).await?; + } else if let Ok(real) = symlink_realpath(&new).await { + ok_or_not_found(fs::remove_file(&new).await)?; + FilesOp::Deleting(parent.clone(), vec![Url::from(real)]).emit(); + fs::File::create(&new).await?; } else { fs::create_dir_all(&parent).await.ok(); ok_or_not_found(fs::remove_file(&new).await)?; @@ -54,7 +58,7 @@ impl Manager { } if let Ok(f) = File::from(new.clone()).await { - FilesOp::Upserting(parent, HashMap::from_iter([(new.clone(), f)])).emit(); + FilesOp::Upserting(parent, HashMap::from_iter([(f.url(), f)])).emit(); TabProxy::reveal(&new) } Ok(()) diff --git a/yazi-core/src/manager/commands/rename.rs b/yazi-core/src/manager/commands/rename.rs index 70b9e3718..6b13878d6 100644 --- a/yazi-core/src/manager/commands/rename.rs +++ b/yazi-core/src/manager/commands/rename.rs @@ -5,7 +5,7 @@ use tokio::fs; use yazi_config::popup::InputCfg; use yazi_dds::Pubsub; use yazi_proxy::{InputProxy, TabProxy, WATCHER}; -use yazi_shared::{event::Cmd, fs::{maybe_exists, File, FilesOp, Url}}; +use yazi_shared::{event::Cmd, fs::{maybe_exists, ok_or_not_found, symlink_realpath, File, FilesOp, Url}}; use crate::manager::Manager; @@ -81,7 +81,13 @@ impl Manager { let Some(p_new) = new.parent_url() else { return Ok(()) }; let _permit = WATCHER.acquire().await.unwrap(); + let overwritten = symlink_realpath(&new).await; fs::rename(&old, &new).await?; + + if let Ok(p) = overwritten { + ok_or_not_found(fs::rename(&p, &new).await)?; + FilesOp::Deleting(p_new.clone(), vec![Url::from(p)]).emit(); + } Pubsub::pub_from_rename(tab, &old, &new); let file = File::from(new.clone()).await?; diff --git a/yazi-core/src/manager/watcher.rs b/yazi-core/src/manager/watcher.rs index 45cef5cb5..7eab05a33 100644 --- a/yazi-core/src/manager/watcher.rs +++ b/yazi-core/src/manager/watcher.rs @@ -8,7 +8,7 @@ use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; use tracing::error; use yazi_plugin::isolate; use yazi_proxy::WATCHER; -use yazi_shared::{fs::{symlink_realpath, File, FilesOp, Url}, RoCell}; +use yazi_shared::{fs::{symlink_realpath_with, File, FilesOp, Url}, RoCell}; use super::Linked; use crate::folder::{Files, Folder}; @@ -111,7 +111,7 @@ impl Watcher { }; let realpath = if file.is_link() { - symlink_realpath(&url, &mut cached).await + symlink_realpath_with(&url, &mut cached).await } else { fs::canonicalize(&url).await.map(Cow::Owned) }; diff --git a/yazi-shared/src/fs/fns.rs b/yazi-shared/src/fs/fns.rs index ea727d0e1..fa67bafae 100644 --- a/yazi-shared/src/fs/fns.rs +++ b/yazi-shared/src/fs/fns.rs @@ -24,23 +24,32 @@ pub fn ok_or_not_found(result: io::Result<()>) -> io::Result<()> { } } +#[inline] +pub async fn symlink_realpath(path: &Path) -> io::Result { + if fs::symlink_metadata(path).await?.is_symlink() { + symlink_realpath_with(path, &mut HashMap::new()).await.map(|p| p.into_owned()) + } else { + fs::canonicalize(path).await + } +} + // realpath(3) without resolving symlinks. This is useful for case-insensitive // filesystems. // // Make sure the file of the path exists and is a symlink. -pub async fn symlink_realpath<'a>( +pub async fn symlink_realpath_with<'a>( path: &'a Path, cached: &'a mut HashMap, ) -> io::Result> { - let Some(parent) = path.parent() else { - return Ok(Cow::Borrowed(path)); - }; - let lowercased: PathBuf = path.as_os_str().to_ascii_lowercase().into(); if lowercased == path { return Ok(Cow::Borrowed(path)); } + let Some(parent) = path.parent() else { + return Ok(Cow::Borrowed(path)); + }; + let case = parent.as_os_str().as_encoded_bytes().iter().any(|&b| b.is_ascii_uppercase()); if !cached.contains_key(parent) { let mut it = fs::read_dir(parent).await?; From 656482d7a485d94e553b8517fd6e667a4814e10b Mon Sep 17 00:00:00 2001 From: sxyazi Date: Thu, 13 Jun 2024 20:37:44 +0800 Subject: [PATCH 3/3] .. --- yazi-core/src/manager/watcher.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yazi-core/src/manager/watcher.rs b/yazi-core/src/manager/watcher.rs index 7eab05a33..79305d402 100644 --- a/yazi-core/src/manager/watcher.rs +++ b/yazi-core/src/manager/watcher.rs @@ -110,12 +110,12 @@ impl Watcher { continue; }; - let realpath = if file.is_link() { + let real = if file.is_link() { symlink_realpath_with(&url, &mut cached).await } else { fs::canonicalize(&url).await.map(Cow::Owned) }; - if !realpath.is_ok_and(|p| p == *url) { + if !real.is_ok_and(|p| p == *url) { FilesOp::Deleting(parent, vec![url]).emit(); continue; }