mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Remove canonicalization functionality from VFS and related components
- Eliminated the `canonicalize` method from the `VfsBackend` trait and its implementations in `InMemoryFs`, `NoopBackend`, and `StdBackend`, simplifying the interface and reducing complexity. - Updated various parts of the codebase to remove references to canonicalization, including tests and prefetch cache handling, ensuring consistency across the project. - Adjusted logging and comments to reflect the removal of path normalization logic, enhancing clarity and maintainability. These changes aim to streamline the VFS implementation by focusing on core functionalities without the overhead of path canonicalization.
This commit is contained in:
parent
65f10f111c
commit
fc8a6e9b9c
21 changed files with 123 additions and 440 deletions
|
|
@ -235,30 +235,6 @@ impl VfsBackend for InMemoryFs {
|
|||
// TODO: We rely on Rojo to prepend cwd to any relative path before storing paths
|
||||
// in MemoFS. The current implementation will error if no prepended absolute path
|
||||
// is found. It really only normalizes paths within the provided path's context.
|
||||
// Example: "/Users/username/project/../other/file.txt" ->
|
||||
// "/Users/username/other/file.txt"
|
||||
// Erroneous example: "/Users/../../other/file.txt" -> "/other/file.txt"
|
||||
// This is not very robust. We should implement proper path normalization here or otherwise
|
||||
// warn if we are missing context and can not fully canonicalize the path correctly.
|
||||
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
|
||||
let mut normalized = PathBuf::new();
|
||||
for component in path.components() {
|
||||
match component {
|
||||
std::path::Component::ParentDir => {
|
||||
normalized.pop();
|
||||
}
|
||||
std::path::Component::CurDir => {}
|
||||
_ => normalized.push(component),
|
||||
}
|
||||
}
|
||||
|
||||
let inner = self.inner.lock().unwrap();
|
||||
match inner.entries.get(&normalized) {
|
||||
Some(_) => Ok(normalized),
|
||||
None => not_found(&normalized),
|
||||
}
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ pub use std_backend::{CriticalErrorHandler, StdBackend, WatcherCriticalError};
|
|||
/// the initial tree build to avoid stale data during live operation.
|
||||
pub struct PrefetchCache {
|
||||
pub files: HashMap<PathBuf, Vec<u8>>,
|
||||
pub canonical: HashMap<PathBuf, PathBuf>,
|
||||
/// `true` = file, `false` = directory. Paths not in the map fall through
|
||||
/// to the backend (e.g. init-file probes for paths that don't exist).
|
||||
pub is_file: HashMap<PathBuf, bool>,
|
||||
|
|
@ -93,7 +92,6 @@ pub trait VfsBackend: sealed::Sealed + Send + 'static {
|
|||
fn metadata(&mut self, path: &Path) -> io::Result<Metadata>;
|
||||
fn remove_file(&mut self, path: &Path) -> io::Result<()>;
|
||||
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
||||
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf>;
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>;
|
||||
fn watch(&mut self, path: &Path) -> io::Result<()>;
|
||||
|
|
@ -278,18 +276,6 @@ impl VfsInner {
|
|||
self.backend.metadata(path)
|
||||
}
|
||||
|
||||
fn canonicalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
|
||||
let path = path.as_ref();
|
||||
|
||||
if let Some(cache) = &mut self.prefetch_cache {
|
||||
if let Some(canonical) = cache.canonical.remove(path) {
|
||||
return Ok(canonical);
|
||||
}
|
||||
}
|
||||
|
||||
self.backend.canonicalize(path)
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
self.backend.event_receiver()
|
||||
}
|
||||
|
|
@ -369,9 +355,9 @@ impl Vfs {
|
|||
|
||||
/// Load a prefetch cache for fast initial reads.
|
||||
///
|
||||
/// File reads and canonicalize calls will check the cache before hitting
|
||||
/// the backend. Call [`clear_prefetch_cache`] after the initial snapshot
|
||||
/// build to free memory and ensure live operations get fresh data.
|
||||
/// File reads will check the cache before hitting the backend. Call
|
||||
/// [`clear_prefetch_cache`] after the initial snapshot build to free
|
||||
/// memory and ensure live operations get fresh data.
|
||||
pub fn set_prefetch_cache(&self, cache: PrefetchCache) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.prefetch_cache = Some(cache);
|
||||
|
|
@ -533,19 +519,6 @@ impl Vfs {
|
|||
self.inner.lock().unwrap().metadata(path)
|
||||
}
|
||||
|
||||
/// Normalize a path via the underlying backend.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::canonicalize`][std::fs::canonicalize]. Relative paths are
|
||||
/// resolved against the backend's current working directory (if applicable) and errors are
|
||||
/// surfaced directly from the backend.
|
||||
///
|
||||
/// [std::fs::canonicalize]: https://doc.rust-lang.org/stable/std/fs/fn.canonicalize.html
|
||||
#[inline]
|
||||
pub fn canonicalize<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
|
||||
let path = path.as_ref();
|
||||
self.inner.lock().unwrap().canonicalize(path)
|
||||
}
|
||||
|
||||
/// Retrieve a handle to the event receiver for this `Vfs`.
|
||||
#[inline]
|
||||
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
|
|
@ -673,13 +646,6 @@ impl VfsLock<'_> {
|
|||
self.inner.metadata(path)
|
||||
}
|
||||
|
||||
/// Normalize a path via the underlying backend.
|
||||
#[inline]
|
||||
pub fn normalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
|
||||
let path = path.as_ref();
|
||||
self.inner.canonicalize(path)
|
||||
}
|
||||
|
||||
/// Retrieve a handle to the event receiver for this `Vfs`.
|
||||
#[inline]
|
||||
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
|
|
@ -715,74 +681,12 @@ mod test {
|
|||
);
|
||||
}
|
||||
|
||||
/// https://github.com/rojo-rbx/rojo/issues/1200
|
||||
#[test]
|
||||
fn canonicalize_in_memory_success() {
|
||||
let mut imfs = InMemoryFs::new();
|
||||
let contents = "Lorem ipsum dolor sit amet.".to_string();
|
||||
|
||||
imfs.load_snapshot("/test/file.txt", VfsSnapshot::file(contents.to_string()))
|
||||
.unwrap();
|
||||
|
||||
let vfs = Vfs::new(imfs);
|
||||
|
||||
assert_eq!(
|
||||
vfs.canonicalize("/test/nested/../file.txt").unwrap(),
|
||||
PathBuf::from("/test/file.txt")
|
||||
);
|
||||
assert_eq!(
|
||||
vfs.read_to_string(vfs.canonicalize("/test/nested/../file.txt").unwrap())
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
contents.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_in_memory_missing_errors() {
|
||||
let imfs = InMemoryFs::new();
|
||||
let vfs = Vfs::new(imfs);
|
||||
|
||||
let err = vfs.canonicalize("test").unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_std_backend_success() {
|
||||
let contents = "Lorem ipsum dolor sit amet.".to_string();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file_path = dir.path().join("file.txt");
|
||||
fs_err::write(&file_path, contents.to_string()).unwrap();
|
||||
|
||||
let vfs = Vfs::new(StdBackend::new_for_testing());
|
||||
let canonicalized = vfs.canonicalize(&file_path).unwrap();
|
||||
assert_eq!(canonicalized, file_path.canonicalize().unwrap());
|
||||
assert_eq!(
|
||||
vfs.read_to_string(&canonicalized).unwrap().to_string(),
|
||||
contents.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_std_backend_missing_errors() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file_path = dir.path().join("test");
|
||||
|
||||
let vfs = Vfs::new(StdBackend::new_for_testing());
|
||||
let err = vfs.canonicalize(&file_path).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
||||
}
|
||||
|
||||
fn make_prefetch(files: Vec<(&str, &[u8])>, canonical: Vec<(&str, &str)>) -> PrefetchCache {
|
||||
fn make_prefetch(files: Vec<(&str, &[u8])>) -> PrefetchCache {
|
||||
PrefetchCache {
|
||||
files: files
|
||||
.into_iter()
|
||||
.map(|(k, v)| (PathBuf::from(k), v.to_vec()))
|
||||
.collect(),
|
||||
canonical: canonical
|
||||
.into_iter()
|
||||
.map(|(k, v)| (PathBuf::from(k), PathBuf::from(v)))
|
||||
.collect(),
|
||||
is_file: HashMap::new(),
|
||||
children: HashMap::new(),
|
||||
}
|
||||
|
|
@ -792,7 +696,7 @@ mod test {
|
|||
fn prefetch_cache_read_hit() {
|
||||
let imfs = InMemoryFs::new();
|
||||
let vfs = Vfs::new(imfs);
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")], vec![]));
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")]));
|
||||
|
||||
assert_eq!(vfs.read("test").unwrap().as_slice(), b"cached");
|
||||
}
|
||||
|
|
@ -803,7 +707,7 @@ mod test {
|
|||
imfs.load_snapshot("test", VfsSnapshot::file("backend"))
|
||||
.unwrap();
|
||||
let vfs = Vfs::new(imfs);
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")], vec![]));
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")]));
|
||||
|
||||
assert_eq!(vfs.read("test").unwrap().as_slice(), b"cached");
|
||||
assert_eq!(vfs.read("test").unwrap().as_slice(), b"backend");
|
||||
|
|
@ -815,7 +719,7 @@ mod test {
|
|||
imfs.load_snapshot("other", VfsSnapshot::file("backend"))
|
||||
.unwrap();
|
||||
let vfs = Vfs::new(imfs);
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")], vec![]));
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")]));
|
||||
|
||||
assert_eq!(vfs.read("other").unwrap().as_slice(), b"backend");
|
||||
}
|
||||
|
|
@ -824,7 +728,7 @@ mod test {
|
|||
fn prefetch_cache_read_to_string_hit() {
|
||||
let imfs = InMemoryFs::new();
|
||||
let vfs = Vfs::new(imfs);
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"hello")], vec![]));
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"hello")]));
|
||||
|
||||
assert_eq!(vfs.read_to_string("test").unwrap().as_str(), "hello");
|
||||
}
|
||||
|
|
@ -833,7 +737,7 @@ mod test {
|
|||
fn prefetch_cache_lf_normalized_hit() {
|
||||
let imfs = InMemoryFs::new();
|
||||
let vfs = Vfs::new(imfs);
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"line1\r\nline2\r\n")], vec![]));
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"line1\r\nline2\r\n")]));
|
||||
|
||||
assert_eq!(
|
||||
vfs.read_to_string_lf_normalized("test").unwrap().as_str(),
|
||||
|
|
@ -841,39 +745,13 @@ mod test {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefetch_cache_canonicalize_hit() {
|
||||
let imfs = InMemoryFs::new();
|
||||
let vfs = Vfs::new(imfs);
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![], vec![("src/foo", "/abs/src/foo")]));
|
||||
|
||||
assert_eq!(
|
||||
vfs.canonicalize("src/foo").unwrap(),
|
||||
PathBuf::from("/abs/src/foo")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefetch_cache_canonicalize_miss_falls_through() {
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot("/real/file.txt", VfsSnapshot::file("x"))
|
||||
.unwrap();
|
||||
let vfs = Vfs::new(imfs);
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![], vec![("other", "/abs/other")]));
|
||||
|
||||
assert_eq!(
|
||||
vfs.canonicalize("/real/file.txt").unwrap(),
|
||||
PathBuf::from("/real/file.txt")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefetch_cache_clear_restores_backend() {
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot("test", VfsSnapshot::file("backend"))
|
||||
.unwrap();
|
||||
let vfs = Vfs::new(imfs);
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")], vec![]));
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")]));
|
||||
vfs.clear_prefetch_cache();
|
||||
|
||||
assert_eq!(vfs.read("test").unwrap().as_slice(), b"backend");
|
||||
|
|
@ -914,45 +792,19 @@ mod test {
|
|||
fn prefetch_cache_read_to_string_invalid_utf8() {
|
||||
let imfs = InMemoryFs::new();
|
||||
let vfs = Vfs::new(imfs);
|
||||
vfs.set_prefetch_cache(make_prefetch(
|
||||
vec![("test", &[0xFF, 0xFE, 0x00, 0x80])],
|
||||
vec![],
|
||||
));
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("test", &[0xFF, 0xFE, 0x00, 0x80])]));
|
||||
|
||||
let err = vfs.read_to_string("test").unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefetch_cache_canonicalize_depletion() {
|
||||
let mut imfs = InMemoryFs::new();
|
||||
imfs.load_snapshot("/real/path", VfsSnapshot::file("x"))
|
||||
.unwrap();
|
||||
let vfs = Vfs::new(imfs);
|
||||
vfs.set_prefetch_cache(make_prefetch(
|
||||
vec![],
|
||||
vec![("/real/path", "/canonical/path")],
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
vfs.canonicalize("/real/path").unwrap(),
|
||||
PathBuf::from("/canonical/path"),
|
||||
"First call should hit cache"
|
||||
);
|
||||
assert_eq!(
|
||||
vfs.canonicalize("/real/path").unwrap(),
|
||||
PathBuf::from("/real/path"),
|
||||
"Second call should fall through to InMemoryFs backend"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefetch_cache_set_overwrite_previous() {
|
||||
let imfs = InMemoryFs::new();
|
||||
let vfs = Vfs::new(imfs);
|
||||
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("a", b"first")], vec![]));
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("a", b"second")], vec![]));
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("a", b"first")]));
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("a", b"second")]));
|
||||
|
||||
assert_eq!(vfs.read("a").unwrap().as_slice(), b"second");
|
||||
}
|
||||
|
|
@ -1003,22 +855,17 @@ mod test {
|
|||
fn prefetch_cache_many_files_std_backend() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut cache_files = HashMap::new();
|
||||
let mut canonical = HashMap::new();
|
||||
|
||||
for i in 0..50 {
|
||||
let path = dir.path().join(format!("f{i}.txt"));
|
||||
let content = format!("data_{i}");
|
||||
fs_err::write(&path, &content).unwrap();
|
||||
cache_files.insert(path.clone(), content.into_bytes());
|
||||
if let Ok(c) = path.canonicalize() {
|
||||
canonical.insert(path, c);
|
||||
}
|
||||
}
|
||||
|
||||
let vfs = Vfs::new(StdBackend::new_for_testing());
|
||||
vfs.set_prefetch_cache(PrefetchCache {
|
||||
files: cache_files,
|
||||
canonical,
|
||||
is_file: HashMap::new(),
|
||||
children: HashMap::new(),
|
||||
});
|
||||
|
|
@ -1046,7 +893,7 @@ mod test {
|
|||
imfs.load_snapshot("test", VfsSnapshot::file("original"))
|
||||
.unwrap();
|
||||
let vfs = Vfs::new(imfs);
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")], vec![]));
|
||||
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")]));
|
||||
|
||||
vfs.write("test", b"written").unwrap();
|
||||
let data = vfs.read("test").unwrap();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{Metadata, ReadDir, VfsBackend, VfsEvent};
|
||||
|
||||
|
|
@ -50,10 +50,6 @@ impl VfsBackend for NoopBackend {
|
|||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn canonicalize(&mut self, _path: &Path) -> io::Result<PathBuf> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
crossbeam_channel::never()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -324,10 +324,6 @@ impl VfsBackend for StdBackend {
|
|||
})
|
||||
}
|
||||
|
||||
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
|
||||
fs_err::canonicalize(path)
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
self.watcher_receiver.clone()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,11 +99,6 @@ impl ChangeProcessor {
|
|||
// (non-serve commands). never() blocks forever without selecting.
|
||||
let critical_error_receiver =
|
||||
critical_error_receiver.unwrap_or_else(crossbeam_channel::never);
|
||||
// Canonicalize project_root so path comparisons work with the
|
||||
// \\?\ prefix that std::fs::canonicalize adds on Windows.
|
||||
let project_root = std::fs::canonicalize(&project_root).unwrap_or(project_root);
|
||||
let project_file_path =
|
||||
std::fs::canonicalize(&project_file_path).unwrap_or(project_file_path);
|
||||
let task = JobThreadContext {
|
||||
tree,
|
||||
vfs,
|
||||
|
|
@ -306,23 +301,8 @@ impl JobThreadContext {
|
|||
None
|
||||
}
|
||||
|
||||
/// Canonicalize a path for use as a suppression map key.
|
||||
fn suppression_key(path: &Path) -> PathBuf {
|
||||
if let Ok(canonical) = std::fs::canonicalize(path) {
|
||||
canonical
|
||||
} else if let Some(parent) = path.parent() {
|
||||
if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
|
||||
if let Some(file_name) = path.file_name() {
|
||||
canonical_parent.join(file_name)
|
||||
} else {
|
||||
path.to_path_buf()
|
||||
}
|
||||
} else {
|
||||
path.to_path_buf()
|
||||
}
|
||||
} else {
|
||||
path.to_path_buf()
|
||||
}
|
||||
path.to_path_buf()
|
||||
}
|
||||
|
||||
/// Suppress the next Create/Write VFS event for the given path.
|
||||
|
|
@ -686,49 +666,33 @@ impl JobThreadContext {
|
|||
_ => None,
|
||||
};
|
||||
if let Some(ref path) = event_path {
|
||||
let canonical = std::fs::canonicalize(path).ok();
|
||||
let mut suppressed = self.suppressed_paths.lock().unwrap();
|
||||
// Determine which key matches: try canonical first (most likely
|
||||
// to match what suppress_path stored), then fall back to raw.
|
||||
let matched_key = canonical
|
||||
.as_ref()
|
||||
.filter(|c| suppressed.contains_key(c.as_path()))
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
if suppressed.contains_key(path) {
|
||||
Some(path.clone())
|
||||
} else {
|
||||
None
|
||||
let key = path.clone();
|
||||
if let Some(counts) = suppressed.get_mut(key.as_path()) {
|
||||
let should_suppress = match &event {
|
||||
VfsEvent::Remove(_) if counts.0 > 0 => {
|
||||
counts.0 -= 1;
|
||||
true
|
||||
}
|
||||
});
|
||||
if let Some(key) = matched_key {
|
||||
if let Some(counts) = suppressed.get_mut(&key) {
|
||||
let should_suppress = match &event {
|
||||
VfsEvent::Remove(_) if counts.0 > 0 => {
|
||||
counts.0 -= 1;
|
||||
true
|
||||
}
|
||||
VfsEvent::Create(_) | VfsEvent::Write(_) if counts.1 > 0 => {
|
||||
counts.1 -= 1;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if counts.0 == 0 && counts.1 == 0 {
|
||||
suppressed.remove(&key);
|
||||
}
|
||||
if should_suppress {
|
||||
drop(suppressed);
|
||||
// Still commit so VFS stays consistent, but skip patching.
|
||||
self.vfs
|
||||
.commit_event(&event)
|
||||
.expect("Error applying VFS change");
|
||||
log::info!(
|
||||
"VFS event SUPPRESSED (API syncback echo): {}",
|
||||
self.display_path(path)
|
||||
);
|
||||
return Vec::new();
|
||||
VfsEvent::Create(_) | VfsEvent::Write(_) if counts.1 > 0 => {
|
||||
counts.1 -= 1;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if counts.0 == 0 && counts.1 == 0 {
|
||||
suppressed.remove(&key);
|
||||
}
|
||||
if should_suppress {
|
||||
drop(suppressed);
|
||||
self.vfs
|
||||
.commit_event(&event)
|
||||
.expect("Error applying VFS change");
|
||||
log::info!(
|
||||
"VFS event SUPPRESSED (API syncback echo): {}",
|
||||
self.display_path(path)
|
||||
);
|
||||
return Vec::new();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -763,107 +727,81 @@ impl JobThreadContext {
|
|||
// of the tree. Calculate and apply all of these changes.
|
||||
let applied_patches = match event {
|
||||
VfsEvent::Create(path) | VfsEvent::Write(path) => {
|
||||
match self.vfs.canonicalize(&path) {
|
||||
Ok(canonical_path) => {
|
||||
log::info!(
|
||||
"VFS: canonicalize OK for {} -> {}",
|
||||
self.display_path(&path),
|
||||
self.display_path(&canonical_path)
|
||||
);
|
||||
self.apply_patches(canonical_path)
|
||||
}
|
||||
Err(_) => {
|
||||
// The path doesn't exist on disk. Two possible causes:
|
||||
//
|
||||
// 1. Phantom event from a rename we performed — macOS FSEvents
|
||||
// can deliver a stale CREATE for the old path. If a pending
|
||||
// suppression exists for this path, consume it and skip.
|
||||
//
|
||||
// 2. The file was deleted between the event firing and us
|
||||
// processing it. Fall back to parent-directory canonicalize
|
||||
// (same strategy as the Remove handler) so the tree can
|
||||
// reconcile the disappearance.
|
||||
let consumed = {
|
||||
let key = Self::suppression_key(&path);
|
||||
let mut suppressed = self.suppressed_paths.lock().unwrap();
|
||||
if let Some(counts) = suppressed.get_mut(&key) {
|
||||
if counts.0 > 0 || counts.1 > 0 {
|
||||
// Drain whichever counter is available
|
||||
if counts.1 > 0 {
|
||||
counts.1 -= 1;
|
||||
} else {
|
||||
counts.0 -= 1;
|
||||
}
|
||||
if counts.0 == 0 && counts.1 == 0 {
|
||||
suppressed.remove(&key);
|
||||
}
|
||||
true
|
||||
if path.exists() {
|
||||
self.apply_patches(path)
|
||||
} else {
|
||||
// The path doesn't exist on disk. Two possible causes:
|
||||
//
|
||||
// 1. Phantom event from a rename we performed — macOS FSEvents
|
||||
// can deliver a stale CREATE for the old path. If a pending
|
||||
// suppression exists for this path, consume it and skip.
|
||||
//
|
||||
// 2. The file was deleted between the event firing and us
|
||||
// processing it. Fall back to parent directory so the tree
|
||||
// can reconcile the disappearance.
|
||||
let consumed = {
|
||||
let key = Self::suppression_key(&path);
|
||||
let mut suppressed = self.suppressed_paths.lock().unwrap();
|
||||
if let Some(counts) = suppressed.get_mut(&key) {
|
||||
if counts.0 > 0 || counts.1 > 0 {
|
||||
if counts.1 > 0 {
|
||||
counts.1 -= 1;
|
||||
} else {
|
||||
false
|
||||
counts.0 -= 1;
|
||||
}
|
||||
if counts.0 == 0 && counts.1 == 0 {
|
||||
suppressed.remove(&key);
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if consumed {
|
||||
if consumed {
|
||||
log::info!(
|
||||
"VFS: phantom Create/Write for non-existent {} — \
|
||||
consumed pending suppression (likely stale rename event)",
|
||||
self.display_path(&path)
|
||||
);
|
||||
Vec::new()
|
||||
} else {
|
||||
let parent = path.parent().unwrap();
|
||||
let file_name = path.file_name().unwrap();
|
||||
if parent.exists() {
|
||||
let resolved = parent.join(file_name);
|
||||
log::info!(
|
||||
"VFS: phantom Create/Write for non-existent {} — \
|
||||
consumed pending suppression (likely stale rename event)",
|
||||
self.display_path(&path)
|
||||
"VFS: Create/Write for vanished file {} — \
|
||||
resolved via parent to {}",
|
||||
self.display_path(&path),
|
||||
self.display_path(&resolved)
|
||||
);
|
||||
self.apply_patches(resolved)
|
||||
} else {
|
||||
log::info!(
|
||||
"VFS: Skipping Create/Write for {} — \
|
||||
parent no longer exists",
|
||||
self.display_path(&path),
|
||||
);
|
||||
Vec::new()
|
||||
} else {
|
||||
// No suppression — the file genuinely vanished. Use the
|
||||
// parent-directory fallback so the tree can reconcile.
|
||||
let parent = path.parent().unwrap();
|
||||
let file_name = path.file_name().unwrap();
|
||||
match self.vfs.canonicalize(parent) {
|
||||
Ok(parent_normalized) => {
|
||||
let resolved = parent_normalized.join(file_name);
|
||||
log::info!(
|
||||
"VFS: Create/Write for vanished file {} — \
|
||||
resolved via parent to {}",
|
||||
self.display_path(&path),
|
||||
self.display_path(&resolved)
|
||||
);
|
||||
self.apply_patches(resolved)
|
||||
}
|
||||
Err(err) => {
|
||||
log::info!(
|
||||
"VFS: Skipping Create/Write for {} — \
|
||||
parent no longer exists: {}",
|
||||
self.display_path(&path),
|
||||
err
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
VfsEvent::Remove(path) => {
|
||||
// MemoFS does not track parent removals yet, so we can canonicalize
|
||||
// the parent path safely and then append the removed path's file name.
|
||||
// However, if the parent was also deleted (e.g., when deleting a directory
|
||||
// tree), canonicalize will fail - in that case, just skip this event.
|
||||
let parent = path.parent().unwrap();
|
||||
let file_name = path.file_name().unwrap();
|
||||
match self.vfs.canonicalize(parent) {
|
||||
Ok(parent_normalized) => {
|
||||
let resolved = parent_normalized.join(file_name);
|
||||
log::info!("VFS: Remove resolved to {}", self.display_path(&resolved));
|
||||
self.apply_patches(resolved)
|
||||
}
|
||||
Err(err) => {
|
||||
log::info!(
|
||||
"VFS: Skipping remove event for {} — parent no longer exists: {}",
|
||||
self.display_path(&path),
|
||||
err
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
if parent.exists() {
|
||||
log::info!("VFS: Remove for {}", self.display_path(&path));
|
||||
self.apply_patches(path)
|
||||
} else {
|
||||
log::info!(
|
||||
"VFS: Skipping remove event for {} — parent no longer exists",
|
||||
self.display_path(&path),
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
|
|
|
|||
|
|
@ -263,8 +263,7 @@ pub(crate) fn write_sourcemap(
|
|||
) -> anyhow::Result<()> {
|
||||
let tree = session.tree();
|
||||
|
||||
let canonical_project_dir = std::fs::canonicalize(session.root_dir())
|
||||
.unwrap_or_else(|_| session.root_dir().to_path_buf());
|
||||
let canonical_project_dir = session.root_dir().to_path_buf();
|
||||
|
||||
let root_node = recurse_create_node(
|
||||
&tree,
|
||||
|
|
@ -306,8 +305,7 @@ pub(crate) fn write_sourcemap_from_syncback(
|
|||
project_dir: &Path,
|
||||
output: &Path,
|
||||
) -> anyhow::Result<()> {
|
||||
let canonical_project_dir =
|
||||
std::fs::canonicalize(project_dir).unwrap_or_else(|_| project_dir.to_path_buf());
|
||||
let canonical_project_dir = project_dir.to_path_buf();
|
||||
|
||||
let root_node =
|
||||
recurse_create_node_from_dom(dom, dom.root_ref(), instance_paths, &canonical_project_dir);
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ pub struct ServeSession {
|
|||
}
|
||||
|
||||
/// Collect all filesystem paths reachable from the project tree's `$path`
|
||||
/// entries, then read file contents and compute canonical paths in parallel.
|
||||
/// entries, then read file contents in parallel.
|
||||
fn prefetch_project_files(project: &Project) -> io::Result<PrefetchCache> {
|
||||
use rayon::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -153,7 +153,6 @@ fn prefetch_project_files(project: &Project) -> io::Result<PrefetchCache> {
|
|||
if roots.is_empty() {
|
||||
return Ok(PrefetchCache {
|
||||
files: HashMap::new(),
|
||||
canonical: HashMap::new(),
|
||||
is_file: HashMap::new(),
|
||||
children: HashMap::new(),
|
||||
});
|
||||
|
|
@ -208,24 +207,11 @@ fn prefetch_project_files(project: &Project) -> io::Result<PrefetchCache> {
|
|||
.collect();
|
||||
|
||||
let read_elapsed = read_start.elapsed();
|
||||
let canon_start = Instant::now();
|
||||
|
||||
let canonical_data: Vec<_> = entries
|
||||
.par_iter()
|
||||
.filter_map(|e| {
|
||||
let path = e.path().to_path_buf();
|
||||
std::fs::canonicalize(&path).ok().map(|c| (path, c))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let canon_elapsed = canon_start.elapsed();
|
||||
|
||||
log::debug!(
|
||||
"Prefetch I/O: read {} files in {:.1?}, canonicalize {} paths in {:.1?}",
|
||||
"Prefetch I/O: read {} files in {:.1?}",
|
||||
file_data.len(),
|
||||
read_elapsed,
|
||||
canonical_data.len(),
|
||||
canon_elapsed,
|
||||
);
|
||||
|
||||
let mut is_file_map: HashMap<std::path::PathBuf, bool> = HashMap::new();
|
||||
|
|
@ -257,7 +243,6 @@ fn prefetch_project_files(project: &Project) -> io::Result<PrefetchCache> {
|
|||
|
||||
Ok(PrefetchCache {
|
||||
files: file_data.into_iter().collect::<HashMap<_, _>>(),
|
||||
canonical: canonical_data.into_iter().collect::<HashMap<_, _>>(),
|
||||
is_file: is_file_map,
|
||||
children: children_map,
|
||||
})
|
||||
|
|
@ -287,9 +272,8 @@ impl ServeSession {
|
|||
match prefetch_project_files(&root_project) {
|
||||
Ok(cache) => {
|
||||
log::debug!(
|
||||
"Prefetch total: {} files + {} canonical paths in {:.1?}",
|
||||
"Prefetch total: {} files in {:.1?}",
|
||||
cache.files.len(),
|
||||
cache.canonical.len(),
|
||||
prefetch_start.elapsed()
|
||||
);
|
||||
vfs.set_prefetch_cache(cache);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ pub fn snapshot_csv(
|
|||
.metadata(
|
||||
InstanceMetadata::new()
|
||||
.instigating_source(path)
|
||||
.relevant_paths(vec![vfs.canonicalize(path)?]),
|
||||
.relevant_paths(vec![path.to_path_buf()]),
|
||||
);
|
||||
|
||||
AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?;
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ pub fn snapshot_dir_no_meta(
|
|||
children
|
||||
};
|
||||
|
||||
let normalized_path = vfs.canonicalize(path)?;
|
||||
let normalized_path = path.to_path_buf();
|
||||
let relevant_paths = vec![
|
||||
normalized_path.clone(),
|
||||
// TODO: We shouldn't need to know about Lua existing in this
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ pub fn snapshot_json(
|
|||
.metadata(
|
||||
InstanceMetadata::new()
|
||||
.instigating_source(path)
|
||||
.relevant_paths(vec![vfs.canonicalize(path)?])
|
||||
.relevant_paths(vec![path.to_path_buf()])
|
||||
.context(context),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ pub fn snapshot_json_model(
|
|||
snapshot.metadata = snapshot
|
||||
.metadata
|
||||
.instigating_source(path)
|
||||
.relevant_paths(vec![vfs.canonicalize(path)?])
|
||||
.relevant_paths(vec![path.to_path_buf()])
|
||||
.context(context)
|
||||
.specified_id(id)
|
||||
.schema(schema)
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ pub fn snapshot_lua(
|
|||
.metadata(
|
||||
InstanceMetadata::new()
|
||||
.instigating_source(path)
|
||||
.relevant_paths(vec![vfs.canonicalize(path)?])
|
||||
.relevant_paths(vec![path.to_path_buf()])
|
||||
.context(context),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -147,10 +147,7 @@ pub fn snapshot_project(
|
|||
// We SHOULD NOT mark the project file as a relevant path for any
|
||||
// nodes that aren't roots. They'll be updated as part of the project
|
||||
// file being updated.
|
||||
snapshot
|
||||
.metadata
|
||||
.relevant_paths
|
||||
.push(vfs.canonicalize(path)?);
|
||||
snapshot.metadata.relevant_paths.push(path.to_path_buf());
|
||||
|
||||
Ok(Some(snapshot))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ pub fn snapshot_rbxm(
|
|||
.metadata(
|
||||
InstanceMetadata::new()
|
||||
.instigating_source(path)
|
||||
.relevant_paths(vec![vfs.canonicalize(path)?])
|
||||
.relevant_paths(vec![path.to_path_buf()])
|
||||
.context(context),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ pub fn snapshot_rbxmx(
|
|||
.metadata(
|
||||
InstanceMetadata::new()
|
||||
.instigating_source(path)
|
||||
.relevant_paths(vec![vfs.canonicalize(path)?])
|
||||
.relevant_paths(vec![path.to_path_buf()])
|
||||
.context(context),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ pub fn snapshot_toml(
|
|||
.metadata(
|
||||
InstanceMetadata::new()
|
||||
.instigating_source(path)
|
||||
.relevant_paths(vec![vfs.canonicalize(path)?])
|
||||
.relevant_paths(vec![path.to_path_buf()])
|
||||
.context(context),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ pub fn snapshot_txt(
|
|||
.metadata(
|
||||
InstanceMetadata::new()
|
||||
.instigating_source(path)
|
||||
.relevant_paths(vec![vfs.canonicalize(path)?])
|
||||
.relevant_paths(vec![path.to_path_buf()])
|
||||
.context(context),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ pub fn snapshot_yaml(
|
|||
.metadata(
|
||||
InstanceMetadata::new()
|
||||
.instigating_source(path)
|
||||
.relevant_paths(vec![vfs.canonicalize(path)?])
|
||||
.relevant_paths(vec![path.to_path_buf()])
|
||||
.context(context),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -254,15 +254,6 @@ pub fn syncback_loop_with_stats(
|
|||
|
||||
let project_path = project.folder_location();
|
||||
|
||||
fn strip_unc_prefix(p: PathBuf) -> PathBuf {
|
||||
let path_str = p.to_string_lossy();
|
||||
if path_str.starts_with(r"\\?\") || path_str.starts_with("//?/") {
|
||||
PathBuf::from(&path_str[4..])
|
||||
} else {
|
||||
p
|
||||
}
|
||||
}
|
||||
|
||||
let phase_timer = std::time::Instant::now();
|
||||
let existing_paths: HashSet<PathBuf> = if !incremental {
|
||||
let mut paths = HashSet::new();
|
||||
|
|
@ -407,10 +398,7 @@ pub fn syncback_loop_with_stats(
|
|||
continue;
|
||||
}
|
||||
}
|
||||
let canonical_dir = std::fs::canonicalize(dir)
|
||||
.map(strip_unc_prefix)
|
||||
.unwrap_or_else(|_| dir.clone());
|
||||
for entry in walkdir::WalkDir::new(&canonical_dir)
|
||||
for entry in walkdir::WalkDir::new(dir)
|
||||
.follow_links(true)
|
||||
.into_iter()
|
||||
.filter_entry(|e| {
|
||||
|
|
@ -661,14 +649,10 @@ pub fn syncback_loop_with_stats(
|
|||
if !incremental && !existing_paths.is_empty() {
|
||||
log::debug!("Clean mode: checking for orphaned files to remove");
|
||||
|
||||
let canonical_project_path = strip_unc_prefix(
|
||||
std::fs::canonicalize(project_path).unwrap_or_else(|_| project_path.to_path_buf()),
|
||||
);
|
||||
|
||||
let added_paths: HashSet<PathBuf> = fs_snapshot
|
||||
.added_paths()
|
||||
.into_iter()
|
||||
.map(|p| strip_unc_prefix(canonical_project_path.join(p)))
|
||||
.map(|p| project_path.join(p))
|
||||
.collect();
|
||||
|
||||
let mut added_dir_prefixes: HashSet<PathBuf> = HashSet::new();
|
||||
|
|
@ -681,10 +665,7 @@ pub fn syncback_loop_with_stats(
|
|||
}
|
||||
}
|
||||
|
||||
let project_file = strip_unc_prefix(
|
||||
std::fs::canonicalize(&project.file_location)
|
||||
.unwrap_or_else(|_| project.file_location.clone()),
|
||||
);
|
||||
let project_file = project.file_location.clone();
|
||||
|
||||
// Collect ALL paths explicitly referenced via $path in the project.
|
||||
// These paths should NOT be removed during orphan cleanup because they
|
||||
|
|
@ -706,12 +687,9 @@ pub fn syncback_loop_with_stats(
|
|||
) {
|
||||
if let Some(ref path_node) = node.path {
|
||||
let resolved = base_path.join(path_node.path());
|
||||
let canonical = std::fs::canonicalize(&resolved)
|
||||
.map(strip_unc_prefix)
|
||||
.unwrap_or_else(|_| resolved.clone());
|
||||
protected.insert(canonical.clone());
|
||||
protected.insert(resolved.clone());
|
||||
if !instance_path.is_empty() {
|
||||
mappings.push((canonical, instance_path.to_string()));
|
||||
mappings.push((resolved, instance_path.to_string()));
|
||||
}
|
||||
}
|
||||
for (name, child) in &node.children {
|
||||
|
|
@ -881,9 +859,7 @@ pub fn syncback_loop_with_stats(
|
|||
}
|
||||
current_ancestor = Some(old_path);
|
||||
|
||||
let relative_path = old_path
|
||||
.strip_prefix(&canonical_project_path)
|
||||
.unwrap_or(old_path);
|
||||
let relative_path = old_path.strip_prefix(project_path).unwrap_or(old_path);
|
||||
|
||||
log::debug!("Removing orphaned path: {}", relative_path.display());
|
||||
if old_path.is_dir() {
|
||||
|
|
|
|||
|
|
@ -358,23 +358,8 @@ impl ApiService {
|
|||
}
|
||||
}
|
||||
|
||||
/// Canonicalize a path for use as a suppression map key.
|
||||
fn suppression_key(path: &Path) -> PathBuf {
|
||||
if let Ok(canonical) = std::fs::canonicalize(path) {
|
||||
canonical
|
||||
} else if let Some(parent) = path.parent() {
|
||||
if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
|
||||
if let Some(file_name) = path.file_name() {
|
||||
canonical_parent.join(file_name)
|
||||
} else {
|
||||
path.to_path_buf()
|
||||
}
|
||||
} else {
|
||||
path.to_path_buf()
|
||||
}
|
||||
} else {
|
||||
path.to_path_buf()
|
||||
}
|
||||
path.to_path_buf()
|
||||
}
|
||||
|
||||
/// Suppress the next Create/Write VFS event for the given path.
|
||||
|
|
|
|||
|
|
@ -241,22 +241,17 @@ fn parallel_snapshot_with_prefetch_cache() {
|
|||
|
||||
use std::collections::HashMap;
|
||||
let mut files = HashMap::new();
|
||||
let mut canonical = HashMap::new();
|
||||
for entry in walkdir::WalkDir::new(root).follow_links(true) {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path().to_path_buf();
|
||||
if entry.file_type().is_file() {
|
||||
files.insert(path.clone(), fs::read(&path).unwrap());
|
||||
}
|
||||
if let Ok(c) = fs::canonicalize(&path) {
|
||||
canonical.insert(path, c);
|
||||
}
|
||||
}
|
||||
|
||||
let vfs_cached = memofs::Vfs::new_default();
|
||||
vfs_cached.set_prefetch_cache(memofs::PrefetchCache {
|
||||
files,
|
||||
canonical,
|
||||
is_file: std::collections::HashMap::new(),
|
||||
children: std::collections::HashMap::new(),
|
||||
});
|
||||
|
|
@ -336,7 +331,6 @@ fn overlapping_path_roots_no_duplicate_children() {
|
|||
|
||||
use std::collections::HashMap;
|
||||
let mut files = HashMap::new();
|
||||
let mut canonical = HashMap::new();
|
||||
let mut is_file = HashMap::new();
|
||||
let mut children_map: HashMap<std::path::PathBuf, Vec<std::path::PathBuf>> = HashMap::new();
|
||||
|
||||
|
|
@ -347,9 +341,6 @@ fn overlapping_path_roots_no_duplicate_children() {
|
|||
if entry.file_type().is_file() {
|
||||
files.insert(path.clone(), fs::read(&path).unwrap());
|
||||
}
|
||||
if let Ok(c) = fs::canonicalize(&path) {
|
||||
canonical.insert(path.clone(), c);
|
||||
}
|
||||
if entry.depth() > 0 {
|
||||
if let Some(parent) = entry.path().parent() {
|
||||
children_map
|
||||
|
|
@ -366,7 +357,6 @@ fn overlapping_path_roots_no_duplicate_children() {
|
|||
let vfs = memofs::Vfs::new_default();
|
||||
vfs.set_prefetch_cache(memofs::PrefetchCache {
|
||||
files,
|
||||
canonical,
|
||||
is_file,
|
||||
children: children_map,
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue