fix: file watcher didn't handle realname resolution used for case-insensitive file systems correctly (#1179)

This commit is contained in:
三咲雅 · Misaki Masa 2024-06-21 18:43:43 +08:00 committed by GitHub
parent 0f84717a1b
commit f5a7aceac0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 75 additions and 40 deletions

View File

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

View File

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

View File

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