diff --git a/cspell.json b/cspell.json index c7b1bfd3..aed43d6c 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"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 +{"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","realname"],"language":"en"} \ No newline at end of file diff --git a/yazi-core/src/manager/watcher.rs b/yazi-core/src/manager/watcher.rs index 79305d40..1ac30c32 100644 --- a/yazi-core/src/manager/watcher.rs +++ b/yazi-core/src/manager/watcher.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, collections::{HashMap, HashSet}, time::{Duration, SystemTime}}; +use std::{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::{symlink_realpath_with, File, FilesOp, Url}, RoCell}; +use yazi_shared::{fs::{symlink_realname, File, FilesOp, Url}, RoCell}; use super::Linked; use crate::folder::{Files, Folder}; @@ -104,18 +104,18 @@ impl Watcher { 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(); continue; }; - let real = if file.is_link() { - symlink_realpath_with(&url, &mut cached).await - } else { - fs::canonicalize(&url).await.map(Cow::Owned) - }; - if !real.is_ok_and(|p| p == *url) { + 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); + + if !eq { FilesOp::Deleting(parent, vec![url]).emit(); continue; } diff --git a/yazi-shared/src/fs/fns.rs b/yazi-shared/src/fs/fns.rs index 9b3c9be8..b8c6500d 100644 --- a/yazi-shared/src/fs/fns.rs +++ b/yazi-shared/src/fs/fns.rs @@ -1,6 +1,6 @@ -use std::{borrow::Cow, collections::{HashMap, VecDeque}, fs::Metadata, path::{Path, PathBuf}}; +use std::{borrow::Cow, collections::{HashMap, VecDeque}, ffi::{OsStr, OsString}, fs::Metadata, path::{Path, PathBuf}}; -use anyhow::Result; +use anyhow::{bail, Result}; use tokio::{fs, io, select, sync::{mpsc, oneshot}, time}; #[inline] @@ -23,50 +23,85 @@ 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 +pub async fn symlink_realpath(path: &Path) -> Result { + let p = fs::canonicalize(path).await?; + if p == path { + return Ok(p); } + + let Some(parent) = path.parent() else { bail!("no parent") }; + symlink_realname(path, &mut HashMap::new()).await.map(|n| parent.join(n)) +} + +#[cfg(unix)] +#[tokio::test] +async fn test_symlink_realpath() { + 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(); + fs::File::create("/tmp/issue-1173/b").await.unwrap(); + fs::File::create("/tmp/issue-1173/real-dir/C").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(); + + 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); + } + + check("/tmp/issue-1173/a", "/tmp/issue-1173/A").await; + check("/tmp/issue-1173/A", "/tmp/issue-1173/A").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/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/d", "/tmp/issue-1173/D").await; + check("/tmp/issue-1173/D", "/tmp/issue-1173/D").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_with<'a>( +// Make sure the file of the path exists. +pub async fn symlink_realname<'a>( path: &'a Path, - cached: &'a mut HashMap, -) -> io::Result> { - let lowercased: PathBuf = path.as_os_str().to_ascii_lowercase().into(); - if lowercased == path { - return Ok(Cow::Borrowed(path)); - } + cached: &'a mut HashMap>, +) -> Result> { + let Some(name) = path.file_name() else { bail!("no file name") }; + let Some(parent) = path.parent() else { return Ok(name.into()) }; - 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 map = HashMap::new(); 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); + 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); } } - cached.insert(parent.to_owned(), PathBuf::new()); + cached.insert(parent.to_owned(), map); } - Ok( - cached - .get(&lowercased) - .filter(|p| !p.as_os_str().is_empty()) - .map_or_else(|| Cow::Borrowed(path), |p| Cow::Borrowed(p)), - ) + let c = &cached[parent]; + if let Some(s) = c.get(name) { + return if s.is_empty() { Ok(name.into()) } else { Ok(s.into()) }; + } + + 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 {