mirror of
https://github.com/sxyazi/yazi.git
synced 2024-09-11 10:26:35 +03:00
fix: file watcher didn't handle realname resolution used for case-insensitive file systems correctly (#1179)
This commit is contained in:
parent
0f84717a1b
commit
f5a7aceac0
@ -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"]}
|
||||
{"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"}
|
@ -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;
|
||||
}
|
||||
|
@ -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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf, PathBuf>,
|
||||
) -> io::Result<Cow<'a, Path>> {
|
||||
let lowercased: PathBuf = path.as_os_str().to_ascii_lowercase().into();
|
||||
if lowercased == path {
|
||||
return Ok(Cow::Borrowed(path));
|
||||
}
|
||||
cached: &'a mut HashMap<PathBuf, HashMap<OsString, 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(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 {
|
||||
|
Loading…
Reference in New Issue
Block a user