mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-23 06:05:24 +00:00
Batch rename: imfs -> vfs
This commit is contained in:
52
src/vfs/error.rs
Normal file
52
src/vfs/error.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use std::{fmt, io, path::PathBuf};
|
||||
|
||||
use failure::Fail;
|
||||
|
||||
pub type FsResult<T> = Result<T, FsError>;
|
||||
pub use io::ErrorKind as FsErrorKind;
|
||||
|
||||
pub trait FsResultExt<T> {
|
||||
fn with_not_found(self) -> Result<Option<T>, FsError>;
|
||||
}
|
||||
|
||||
impl<T> FsResultExt<T> for Result<T, FsError> {
|
||||
fn with_not_found(self) -> Result<Option<T>, FsError> {
|
||||
match self {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(ref err) if err.kind() == FsErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around io::Error that also attaches the path associated with the
|
||||
/// error.
|
||||
#[derive(Debug, Fail)]
|
||||
pub struct FsError {
|
||||
#[fail(cause)]
|
||||
inner: io::Error,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl FsError {
|
||||
pub fn new<P: Into<PathBuf>>(inner: io::Error, path: P) -> FsError {
|
||||
FsError {
|
||||
inner,
|
||||
path: path.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> FsErrorKind {
|
||||
self.inner.kind()
|
||||
}
|
||||
|
||||
pub fn into_raw(self) -> (io::Error, PathBuf) {
|
||||
(self.inner, self.path)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FsError {
|
||||
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(output, "{}: {}", self.path.display(), self.inner)
|
||||
}
|
||||
}
|
||||
8
src/vfs/event.rs
Normal file
8
src/vfs/event.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum VfsEvent {
|
||||
Modified(PathBuf),
|
||||
Created(PathBuf),
|
||||
Removed(PathBuf),
|
||||
}
|
||||
36
src/vfs/fetcher.rs
Normal file
36
src/vfs/fetcher.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
|
||||
use super::event::VfsEvent;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FileType {
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
/// The generic interface that `Vfs` uses to lazily read files from the disk.
|
||||
/// In tests, it's stubbed out to do different versions of absolutely nothing
|
||||
/// depending on the test.
|
||||
pub trait VfsFetcher {
|
||||
fn file_type(&mut self, path: &Path) -> io::Result<FileType>;
|
||||
fn read_children(&mut self, path: &Path) -> io::Result<Vec<PathBuf>>;
|
||||
fn read_contents(&mut self, path: &Path) -> io::Result<Vec<u8>>;
|
||||
|
||||
fn create_directory(&mut self, path: &Path) -> io::Result<()>;
|
||||
fn write_file(&mut self, path: &Path, contents: &[u8]) -> io::Result<()>;
|
||||
fn remove(&mut self, path: &Path) -> io::Result<()>;
|
||||
|
||||
fn watch(&mut self, path: &Path);
|
||||
fn unwatch(&mut self, path: &Path);
|
||||
fn receiver(&self) -> Receiver<VfsEvent>;
|
||||
|
||||
/// A method intended for debugging what paths the fetcher is watching.
|
||||
fn watched_paths(&self) -> Vec<&Path> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
21
src/vfs/mod.rs
Normal file
21
src/vfs/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
mod error;
|
||||
mod event;
|
||||
mod fetcher;
|
||||
mod noop_fetcher;
|
||||
mod real_fetcher;
|
||||
mod snapshot;
|
||||
mod vfs;
|
||||
|
||||
pub use error::*;
|
||||
pub use event::*;
|
||||
pub use fetcher::*;
|
||||
pub use noop_fetcher::*;
|
||||
pub use real_fetcher::*;
|
||||
pub use snapshot::*;
|
||||
pub use vfs::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_fetcher;
|
||||
|
||||
#[cfg(test)]
|
||||
pub use test_fetcher::*;
|
||||
62
src/vfs/noop_fetcher.rs
Normal file
62
src/vfs/noop_fetcher.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
//! Implements the VFS fetcher interface for a fake filesystem using Rust's
|
||||
//! std::fs interface.
|
||||
|
||||
// This interface is only used for testing, so it's okay if it isn't used.
|
||||
#![allow(unused)]
|
||||
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
|
||||
use super::{
|
||||
event::VfsEvent,
|
||||
fetcher::{FileType, VfsFetcher},
|
||||
};
|
||||
|
||||
pub struct NoopFetcher;
|
||||
|
||||
impl VfsFetcher for NoopFetcher {
|
||||
fn file_type(&mut self, _path: &Path) -> io::Result<FileType> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"NoopFetcher always returns NotFound",
|
||||
))
|
||||
}
|
||||
|
||||
fn read_children(&mut self, _path: &Path) -> io::Result<Vec<PathBuf>> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"NoopFetcher always returns NotFound",
|
||||
))
|
||||
}
|
||||
|
||||
fn read_contents(&mut self, _path: &Path) -> io::Result<Vec<u8>> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"NoopFetcher always returns NotFound",
|
||||
))
|
||||
}
|
||||
|
||||
fn create_directory(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_file(&mut self, _path: &Path, _contents: &[u8]) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn watch(&mut self, _path: &Path) {}
|
||||
|
||||
fn unwatch(&mut self, _path: &Path) {}
|
||||
|
||||
fn receiver(&self) -> Receiver<VfsEvent> {
|
||||
crossbeam_channel::never()
|
||||
}
|
||||
}
|
||||
216
src/vfs/real_fetcher.rs
Normal file
216
src/vfs/real_fetcher.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
//! Implements the VFS fetcher interface for the real filesystem using Rust's
|
||||
//! std::fs interface and notify as the file watcher.
|
||||
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
sync::mpsc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||
use jod_thread::JoinHandle;
|
||||
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
|
||||
use super::{
|
||||
event::VfsEvent,
|
||||
fetcher::{FileType, VfsFetcher},
|
||||
};
|
||||
|
||||
/// Workaround to disable the file watcher for processes that don't need it,
|
||||
/// since notify appears hang on to mpsc Sender objects too long, causing Rojo
|
||||
/// to deadlock on drop.
|
||||
///
|
||||
/// We can make constructing the watcher optional in order to hotfix rojo build.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WatchMode {
|
||||
Enabled,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
pub struct RealFetcher {
|
||||
// Drop order is relevant here!
|
||||
//
|
||||
// `watcher` must be dropped before `_converter_thread` or else joining the
|
||||
// thread will cause a deadlock.
|
||||
watcher: Option<RecommendedWatcher>,
|
||||
|
||||
/// Thread handle to convert notify's mpsc channel messages into
|
||||
/// crossbeam_channel messages.
|
||||
_converter_thread: JoinHandle<()>,
|
||||
|
||||
/// The crossbeam receiver filled with events from the converter thread.
|
||||
receiver: Receiver<VfsEvent>,
|
||||
|
||||
/// All of the paths that the fetcher is watching, tracked here because
|
||||
/// notify does not expose this information.
|
||||
watched_paths: HashSet<PathBuf>,
|
||||
}
|
||||
|
||||
impl RealFetcher {
|
||||
pub fn new(watch_mode: WatchMode) -> RealFetcher {
|
||||
log::trace!("Starting RealFetcher with watch mode {:?}", watch_mode);
|
||||
|
||||
let (notify_sender, notify_receiver) = mpsc::channel();
|
||||
let (sender, receiver) = unbounded();
|
||||
|
||||
let handle = jod_thread::Builder::new()
|
||||
.name("notify message converter".to_owned())
|
||||
.spawn(move || {
|
||||
log::trace!("RealFetcher converter thread started");
|
||||
converter_thread(notify_receiver, sender);
|
||||
log::trace!("RealFetcher converter thread stopped");
|
||||
})
|
||||
.expect("Could not start message converter thread");
|
||||
|
||||
// TODO: Investigate why notify hangs onto notify_sender too long,
|
||||
// causing our program to deadlock. Once this is fixed, watcher no
|
||||
// longer needs to be optional, but is still maybe useful?
|
||||
let watcher = match watch_mode {
|
||||
WatchMode::Enabled => Some(
|
||||
notify::watcher(notify_sender, Duration::from_millis(300))
|
||||
.expect("Couldn't start 'notify' file watcher"),
|
||||
),
|
||||
WatchMode::Disabled => None,
|
||||
};
|
||||
|
||||
RealFetcher {
|
||||
watcher,
|
||||
_converter_thread: handle,
|
||||
receiver,
|
||||
watched_paths: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn converter_thread(notify_receiver: mpsc::Receiver<DebouncedEvent>, sender: Sender<VfsEvent>) {
|
||||
use DebouncedEvent::*;
|
||||
|
||||
for event in notify_receiver {
|
||||
log::trace!("Notify event: {:?}", event);
|
||||
|
||||
match event {
|
||||
Create(path) => sender.send(VfsEvent::Created(path)).unwrap(),
|
||||
Write(path) => sender.send(VfsEvent::Modified(path)).unwrap(),
|
||||
Remove(path) => sender.send(VfsEvent::Removed(path)).unwrap(),
|
||||
Rename(from_path, to_path) => {
|
||||
sender.send(VfsEvent::Created(from_path)).unwrap();
|
||||
sender.send(VfsEvent::Removed(to_path)).unwrap();
|
||||
}
|
||||
Rescan => {
|
||||
log::warn!("Unhandled filesystem rescan event.");
|
||||
log::warn!(
|
||||
"Please file an issue! Rojo may need to handle this case, but does not yet."
|
||||
);
|
||||
}
|
||||
Error(err, maybe_path) => {
|
||||
log::warn!("Unhandled filesystem error: {}", err);
|
||||
|
||||
match maybe_path {
|
||||
Some(path) => log::warn!("On path {}", path.display()),
|
||||
None => log::warn!("No path was associated with this error."),
|
||||
}
|
||||
|
||||
log::warn!(
|
||||
"Rojo may need to handle this. If this happens again, please file an issue!"
|
||||
);
|
||||
}
|
||||
NoticeWrite(_) | NoticeRemove(_) | Chmod(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VfsFetcher for RealFetcher {
|
||||
fn file_type(&mut self, path: &Path) -> io::Result<FileType> {
|
||||
let metadata = fs::metadata(path)?;
|
||||
|
||||
if metadata.is_file() {
|
||||
Ok(FileType::File)
|
||||
} else {
|
||||
Ok(FileType::Directory)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_children(&mut self, path: &Path) -> io::Result<Vec<PathBuf>> {
|
||||
log::trace!("Reading directory {}", path.display());
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
let iter = fs::read_dir(path)?;
|
||||
|
||||
for entry in iter {
|
||||
result.push(entry?.path());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn read_contents(&mut self, path: &Path) -> io::Result<Vec<u8>> {
|
||||
log::trace!("Reading file {}", path.display());
|
||||
|
||||
fs::read(path)
|
||||
}
|
||||
|
||||
fn create_directory(&mut self, path: &Path) -> io::Result<()> {
|
||||
log::trace!("Creating directory {}", path.display());
|
||||
|
||||
fs::create_dir(path)
|
||||
}
|
||||
|
||||
fn write_file(&mut self, path: &Path, contents: &[u8]) -> io::Result<()> {
|
||||
log::trace!("Writing path {}", path.display());
|
||||
|
||||
fs::write(path, contents)
|
||||
}
|
||||
|
||||
fn remove(&mut self, path: &Path) -> io::Result<()> {
|
||||
log::trace!("Removing path {}", path.display());
|
||||
|
||||
let metadata = fs::metadata(path)?;
|
||||
|
||||
if metadata.is_file() {
|
||||
fs::remove_file(path)
|
||||
} else {
|
||||
fs::remove_dir_all(path)
|
||||
}
|
||||
}
|
||||
|
||||
fn watch(&mut self, path: &Path) {
|
||||
log::trace!("Watching path {}", path.display());
|
||||
|
||||
if let Some(watcher) = self.watcher.as_mut() {
|
||||
match watcher.watch(path, RecursiveMode::NonRecursive) {
|
||||
Ok(_) => {
|
||||
self.watched_paths.insert(path.to_path_buf());
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("Couldn't watch path {}: {:?}", path.display(), err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn unwatch(&mut self, path: &Path) {
|
||||
log::trace!("Stopped watching path {}", path.display());
|
||||
|
||||
if let Some(watcher) = self.watcher.as_mut() {
|
||||
// Remove the path from our watched paths regardless of the outcome
|
||||
// of notify's unwatch to ensure we drop old paths in the event of a
|
||||
// rename.
|
||||
self.watched_paths.remove(path);
|
||||
|
||||
if let Err(err) = watcher.unwatch(path) {
|
||||
log::warn!("Couldn't unwatch path {}: {:?}", path.display(), err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn receiver(&self) -> Receiver<VfsEvent> {
|
||||
self.receiver.clone()
|
||||
}
|
||||
|
||||
fn watched_paths(&self) -> Vec<&Path> {
|
||||
self.watched_paths.iter().map(|v| v.as_path()).collect()
|
||||
}
|
||||
}
|
||||
42
src/vfs/snapshot.rs
Normal file
42
src/vfs/snapshot.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
// This file is non-critical and used for testing, so it's okay if it's unused.
|
||||
#![allow(unused)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum VfsSnapshot {
|
||||
File(FileSnapshot),
|
||||
Directory(DirectorySnapshot),
|
||||
}
|
||||
|
||||
impl VfsSnapshot {
|
||||
/// Create a new file VfsSnapshot with the given contents.
|
||||
pub fn file(contents: impl Into<Vec<u8>>) -> VfsSnapshot {
|
||||
VfsSnapshot::File(FileSnapshot {
|
||||
contents: contents.into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new directory VfsSnapshot with the given children.
|
||||
pub fn dir<S: Into<String>>(children: HashMap<S, VfsSnapshot>) -> VfsSnapshot {
|
||||
let children = children.into_iter().map(|(k, v)| (k.into(), v)).collect();
|
||||
|
||||
VfsSnapshot::Directory(DirectorySnapshot { children })
|
||||
}
|
||||
|
||||
pub fn empty_dir() -> VfsSnapshot {
|
||||
VfsSnapshot::Directory(DirectorySnapshot {
|
||||
children: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileSnapshot {
|
||||
pub contents: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DirectorySnapshot {
|
||||
pub children: HashMap<String, VfsSnapshot>,
|
||||
}
|
||||
175
src/vfs/test_fetcher.rs
Normal file
175
src/vfs/test_fetcher.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
//! Implements the VFS fetcher interface for a fake filesystem that can be
|
||||
//! mutated and have changes signaled through it.
|
||||
//!
|
||||
//! This is useful for testing how things using Vfs react to changed events
|
||||
//! without relying on the real filesystem implementation, which is very
|
||||
//! platform-specific.
|
||||
|
||||
// This interface is only used for testing, so it's okay if it isn't used.
|
||||
#![allow(unused)]
|
||||
|
||||
use std::{
|
||||
io,
|
||||
path::{self, Path, PathBuf},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||
|
||||
use crate::path_map::PathMap;
|
||||
|
||||
use super::{
|
||||
event::VfsEvent,
|
||||
fetcher::{FileType, VfsFetcher},
|
||||
snapshot::VfsSnapshot,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TestFetcherState {
|
||||
inner: Arc<Mutex<TestFetcherStateInner>>,
|
||||
}
|
||||
|
||||
impl TestFetcherState {
|
||||
pub fn load_snapshot<P: AsRef<Path>>(&self, path: P, snapshot: VfsSnapshot) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.load_snapshot(path.as_ref().to_path_buf(), snapshot);
|
||||
}
|
||||
|
||||
pub fn remove<P: AsRef<Path>>(&self, path: P) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.remove(path.as_ref());
|
||||
}
|
||||
|
||||
pub fn raise_event(&self, event: VfsEvent) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.raise_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub enum TestFetcherEntry {
|
||||
File(Vec<u8>),
|
||||
Dir,
|
||||
}
|
||||
|
||||
struct TestFetcherStateInner {
|
||||
entries: PathMap<TestFetcherEntry>,
|
||||
sender: Sender<VfsEvent>,
|
||||
}
|
||||
|
||||
impl TestFetcherStateInner {
|
||||
fn new(sender: Sender<VfsEvent>) -> Self {
|
||||
let mut entries = PathMap::new();
|
||||
entries.insert(Path::new("/"), TestFetcherEntry::Dir);
|
||||
|
||||
Self { sender, entries }
|
||||
}
|
||||
|
||||
fn load_snapshot(&mut self, path: PathBuf, snapshot: VfsSnapshot) {
|
||||
match snapshot {
|
||||
VfsSnapshot::File(file) => {
|
||||
self.entries
|
||||
.insert(path, TestFetcherEntry::File(file.contents));
|
||||
}
|
||||
VfsSnapshot::Directory(directory) => {
|
||||
self.entries.insert(path.clone(), TestFetcherEntry::Dir);
|
||||
|
||||
for (child_name, child) in directory.children.into_iter() {
|
||||
self.load_snapshot(path.join(child_name), child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&mut self, path: &Path) {
|
||||
self.entries.remove(path);
|
||||
}
|
||||
|
||||
fn raise_event(&mut self, event: VfsEvent) {
|
||||
self.sender.send(event).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestFetcher {
|
||||
state: TestFetcherState,
|
||||
receiver: Receiver<VfsEvent>,
|
||||
}
|
||||
|
||||
impl TestFetcher {
|
||||
pub fn new() -> (TestFetcherState, Self) {
|
||||
let (sender, receiver) = unbounded();
|
||||
|
||||
let state = TestFetcherState {
|
||||
inner: Arc::new(Mutex::new(TestFetcherStateInner::new(sender))),
|
||||
};
|
||||
|
||||
(state.clone(), Self { receiver, state })
|
||||
}
|
||||
}
|
||||
|
||||
impl VfsFetcher for TestFetcher {
|
||||
fn file_type(&mut self, path: &Path) -> io::Result<FileType> {
|
||||
let inner = self.state.inner.lock().unwrap();
|
||||
|
||||
match inner.entries.get(path) {
|
||||
Some(TestFetcherEntry::File(_)) => Ok(FileType::File),
|
||||
Some(TestFetcherEntry::Dir) => Ok(FileType::Directory),
|
||||
None => Err(io::Error::new(io::ErrorKind::NotFound, "Path not found")),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_children(&mut self, path: &Path) -> io::Result<Vec<PathBuf>> {
|
||||
let inner = self.state.inner.lock().unwrap();
|
||||
|
||||
Ok(inner
|
||||
.entries
|
||||
.children(path)
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Path not found"))?
|
||||
.into_iter()
|
||||
.map(|path| path.to_path_buf())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn read_contents(&mut self, path: &Path) -> io::Result<Vec<u8>> {
|
||||
let inner = self.state.inner.lock().unwrap();
|
||||
|
||||
let node = inner.entries.get(path);
|
||||
|
||||
match node {
|
||||
Some(TestFetcherEntry::File(contents)) => Ok(contents.clone()),
|
||||
Some(TestFetcherEntry::Dir) => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Cannot read contents of a directory",
|
||||
)),
|
||||
None => Err(io::Error::new(io::ErrorKind::NotFound, "Path not found")),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_directory(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"TestFetcher is not mutable yet",
|
||||
))
|
||||
}
|
||||
|
||||
fn write_file(&mut self, _path: &Path, _contents: &[u8]) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"TestFetcher is not mutable yet",
|
||||
))
|
||||
}
|
||||
|
||||
fn remove(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"TestFetcher is not mutable yet",
|
||||
))
|
||||
}
|
||||
|
||||
fn watch(&mut self, _path: &Path) {}
|
||||
|
||||
fn unwatch(&mut self, _path: &Path) {}
|
||||
|
||||
fn receiver(&self) -> Receiver<VfsEvent> {
|
||||
self.receiver.clone()
|
||||
}
|
||||
}
|
||||
569
src/vfs/vfs.rs
Normal file
569
src/vfs/vfs.rs
Normal file
@@ -0,0 +1,569 @@
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
|
||||
use crate::path_map::PathMap;
|
||||
|
||||
use super::{
|
||||
error::{FsError, FsResult},
|
||||
event::VfsEvent,
|
||||
fetcher::{FileType, VfsFetcher},
|
||||
snapshot::VfsSnapshot,
|
||||
};
|
||||
|
||||
/// An in-memory filesystem that can be incrementally populated and updated as
|
||||
/// filesystem modification events occur.
|
||||
///
|
||||
/// All operations on the `Vfs` are lazy and do I/O as late as they can to
|
||||
/// avoid reading extraneous files or directories from the disk. This means that
|
||||
/// they all take `self` mutably, and means that it isn't possible to hold
|
||||
/// references to the internal state of the Vfs while traversing it!
|
||||
///
|
||||
/// Most operations return `VfsEntry` objects to work around this, which is
|
||||
/// effectively a index into the `Vfs`.
|
||||
pub struct Vfs<F> {
|
||||
/// A hierarchical map from paths to items that have been read or partially
|
||||
/// read into memory by the Vfs.
|
||||
inner: PathMap<VfsItem>,
|
||||
|
||||
/// This Vfs's fetcher, which is used for all actual interactions with the
|
||||
/// filesystem. It's referred to by the type parameter `F` all over, and is
|
||||
/// generic in order to make it feasible to mock.
|
||||
fetcher: F,
|
||||
}
|
||||
|
||||
impl<F: VfsFetcher> Vfs<F> {
|
||||
pub fn new(fetcher: F) -> Vfs<F> {
|
||||
Vfs {
|
||||
inner: PathMap::new(),
|
||||
fetcher,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_receiver(&self) -> Receiver<VfsEvent> {
|
||||
self.fetcher.receiver()
|
||||
}
|
||||
|
||||
pub fn commit_change(&mut self, event: &VfsEvent) -> FsResult<()> {
|
||||
use VfsEvent::*;
|
||||
|
||||
log::trace!("Committing Vfs change {:?}", event);
|
||||
|
||||
match event {
|
||||
Created(path) | Modified(path) => {
|
||||
self.raise_file_changed(path)?;
|
||||
}
|
||||
Removed(path) => {
|
||||
self.raise_file_removed(path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn raise_file_changed(&mut self, path: impl AsRef<Path>) -> FsResult<()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
if !self.would_be_resident(path) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let new_type = self
|
||||
.fetcher
|
||||
.file_type(path)
|
||||
.map_err(|err| FsError::new(err, path.to_path_buf()))?;
|
||||
|
||||
match self.inner.get_mut(path) {
|
||||
Some(existing_item) => {
|
||||
match (existing_item, &new_type) {
|
||||
(VfsItem::File(existing_file), FileType::File) => {
|
||||
// Invalidate the existing file contents.
|
||||
// We can probably be smarter about this by reading the changed file.
|
||||
existing_file.contents = None;
|
||||
}
|
||||
(VfsItem::Directory(_), FileType::Directory) => {
|
||||
// No changes required, a directory updating doesn't mean anything to us.
|
||||
self.fetcher.watch(path);
|
||||
}
|
||||
(VfsItem::File(_), FileType::Directory) => {
|
||||
self.inner.remove(path);
|
||||
self.inner.insert(
|
||||
path.to_path_buf(),
|
||||
VfsItem::new_from_type(FileType::Directory, path),
|
||||
);
|
||||
self.fetcher.watch(path);
|
||||
}
|
||||
(VfsItem::Directory(_), FileType::File) => {
|
||||
self.inner.remove(path);
|
||||
self.inner.insert(
|
||||
path.to_path_buf(),
|
||||
VfsItem::new_from_type(FileType::File, path),
|
||||
);
|
||||
self.fetcher.unwatch(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.inner
|
||||
.insert(path.to_path_buf(), VfsItem::new_from_type(new_type, path));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn raise_file_removed(&mut self, path: impl AsRef<Path>) -> FsResult<()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
if !self.would_be_resident(path) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.inner.remove(path);
|
||||
self.fetcher.unwatch(path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get(&mut self, path: impl AsRef<Path>) -> FsResult<VfsEntry> {
|
||||
self.read_if_not_exists(path.as_ref())?;
|
||||
|
||||
let item = self.inner.get(path.as_ref()).unwrap();
|
||||
|
||||
let is_file = match item {
|
||||
VfsItem::File(_) => true,
|
||||
VfsItem::Directory(_) => false,
|
||||
};
|
||||
|
||||
Ok(VfsEntry {
|
||||
path: item.path().to_path_buf(),
|
||||
is_file,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_contents(&mut self, path: impl AsRef<Path>) -> FsResult<&[u8]> {
|
||||
let path = path.as_ref();
|
||||
|
||||
self.read_if_not_exists(path)?;
|
||||
|
||||
match self.inner.get_mut(path).unwrap() {
|
||||
VfsItem::File(file) => {
|
||||
if file.contents.is_none() {
|
||||
file.contents = Some(
|
||||
self.fetcher
|
||||
.read_contents(path)
|
||||
.map_err(|err| FsError::new(err, path.to_path_buf()))?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(file.contents.as_ref().unwrap())
|
||||
}
|
||||
VfsItem::Directory(_) => Err(FsError::new(
|
||||
io::Error::new(io::ErrorKind::Other, "Can't read a directory"),
|
||||
path.to_path_buf(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_children(&mut self, path: impl AsRef<Path>) -> FsResult<Vec<VfsEntry>> {
|
||||
let path = path.as_ref();
|
||||
|
||||
self.read_if_not_exists(path)?;
|
||||
|
||||
match self.inner.get_mut(path).unwrap() {
|
||||
VfsItem::Directory(dir) => {
|
||||
self.fetcher.watch(path);
|
||||
|
||||
let enumerated = dir.children_enumerated;
|
||||
|
||||
if enumerated {
|
||||
self.inner
|
||||
.children(path)
|
||||
.unwrap() // TODO: Handle None here, which means the PathMap entry did not exist.
|
||||
.into_iter()
|
||||
.map(PathBuf::from) // Convert paths from &Path to PathBuf
|
||||
.collect::<Vec<PathBuf>>() // Collect all PathBufs, since self.get needs to borrow self mutably.
|
||||
.into_iter()
|
||||
.map(|path| self.get(path))
|
||||
.collect::<FsResult<Vec<VfsEntry>>>()
|
||||
} else {
|
||||
dir.children_enumerated = true;
|
||||
|
||||
self.fetcher
|
||||
.read_children(path)
|
||||
.map_err(|err| FsError::new(err, path.to_path_buf()))?
|
||||
.into_iter()
|
||||
.map(|path| self.get(path))
|
||||
.collect::<FsResult<Vec<VfsEntry>>>()
|
||||
}
|
||||
}
|
||||
VfsItem::File(_) => Err(FsError::new(
|
||||
io::Error::new(io::ErrorKind::Other, "Can't read a directory"),
|
||||
path.to_path_buf(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tells whether the given path, if it were loaded, would be loaded if it
|
||||
/// existed.
|
||||
///
|
||||
/// Returns true if the path is loaded or if its parent is loaded, is a
|
||||
/// directory, and is marked as having been enumerated before.
|
||||
///
|
||||
/// This idea corresponds to whether a file change event should result in
|
||||
/// tangible changes to the in-memory filesystem. If a path would be
|
||||
/// resident, we need to read it, and if its contents were known before, we
|
||||
/// need to update them.
|
||||
fn would_be_resident(&self, path: &Path) -> bool {
|
||||
if self.inner.contains_key(path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
if let Some(VfsItem::Directory(dir)) = self.inner.get(parent) {
|
||||
return !dir.children_enumerated;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Attempts to read the path into the `Vfs` if it doesn't exist.
|
||||
///
|
||||
/// This does not necessitate that file contents or directory children will
|
||||
/// be read. Depending on the `VfsFetcher` implementation that the `Vfs`
|
||||
/// is using, this call may read exactly only the given path and no more.
|
||||
fn read_if_not_exists(&mut self, path: &Path) -> FsResult<()> {
|
||||
if !self.inner.contains_key(path) {
|
||||
let kind = self
|
||||
.fetcher
|
||||
.file_type(path)
|
||||
.map_err(|err| FsError::new(err, path.to_path_buf()))?;
|
||||
|
||||
if kind == FileType::Directory {
|
||||
self.fetcher.watch(path);
|
||||
}
|
||||
|
||||
self.inner
|
||||
.insert(path.to_path_buf(), VfsItem::new_from_type(kind, path));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains extra methods that should only be used for debugging. They're
|
||||
/// broken out into a separate trait to make it more explicit to depend on them.
|
||||
pub trait VfsDebug {
|
||||
fn debug_load_snapshot<P: AsRef<Path>>(&mut self, path: P, snapshot: VfsSnapshot);
|
||||
fn debug_is_file(&self, path: &Path) -> bool;
|
||||
fn debug_contents<'a>(&'a self, path: &Path) -> Option<&'a [u8]>;
|
||||
fn debug_children<'a>(&'a self, path: &Path) -> Option<(bool, Vec<&'a Path>)>;
|
||||
fn debug_orphans(&self) -> Vec<&Path>;
|
||||
fn debug_watched_paths(&self) -> Vec<&Path>;
|
||||
}
|
||||
|
||||
impl<F: VfsFetcher> VfsDebug for Vfs<F> {
|
||||
fn debug_load_snapshot<P: AsRef<Path>>(&mut self, path: P, snapshot: VfsSnapshot) {
|
||||
let path = path.as_ref();
|
||||
|
||||
match snapshot {
|
||||
VfsSnapshot::File(file) => {
|
||||
self.inner.insert(
|
||||
path.to_path_buf(),
|
||||
VfsItem::File(VfsFile {
|
||||
path: path.to_path_buf(),
|
||||
contents: Some(file.contents),
|
||||
}),
|
||||
);
|
||||
}
|
||||
VfsSnapshot::Directory(directory) => {
|
||||
self.inner.insert(
|
||||
path.to_path_buf(),
|
||||
VfsItem::Directory(VfsDirectory {
|
||||
path: path.to_path_buf(),
|
||||
children_enumerated: true,
|
||||
}),
|
||||
);
|
||||
|
||||
for (child_name, child) in directory.children.into_iter() {
|
||||
self.debug_load_snapshot(path.join(child_name), child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_is_file(&self, path: &Path) -> bool {
|
||||
match self.inner.get(path) {
|
||||
Some(VfsItem::File(_)) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_contents<'a>(&'a self, path: &Path) -> Option<&'a [u8]> {
|
||||
match self.inner.get(path) {
|
||||
Some(VfsItem::File(file)) => file.contents.as_ref().map(|vec| vec.as_slice()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_children<'a>(&'a self, path: &Path) -> Option<(bool, Vec<&'a Path>)> {
|
||||
match self.inner.get(path) {
|
||||
Some(VfsItem::Directory(dir)) => {
|
||||
Some((dir.children_enumerated, self.inner.children(path).unwrap()))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_orphans(&self) -> Vec<&Path> {
|
||||
self.inner.orphans().collect()
|
||||
}
|
||||
|
||||
fn debug_watched_paths(&self) -> Vec<&Path> {
|
||||
self.fetcher.watched_paths()
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference to file or folder in an `Vfs`. Can only be produced by the
|
||||
/// entry existing in the Vfs, but can later point to nothing if something
|
||||
/// would invalidate that path.
|
||||
///
|
||||
/// This struct does not borrow from the Vfs since every operation has the
|
||||
/// possibility to mutate the underlying data structure and move memory around.
|
||||
pub struct VfsEntry {
|
||||
path: PathBuf,
|
||||
is_file: bool,
|
||||
}
|
||||
|
||||
impl VfsEntry {
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn contents<'vfs>(&self, vfs: &'vfs mut Vfs<impl VfsFetcher>) -> FsResult<&'vfs [u8]> {
|
||||
vfs.get_contents(&self.path)
|
||||
}
|
||||
|
||||
pub fn children(&self, vfs: &mut Vfs<impl VfsFetcher>) -> FsResult<Vec<VfsEntry>> {
|
||||
vfs.get_children(&self.path)
|
||||
}
|
||||
|
||||
pub fn is_file(&self) -> bool {
|
||||
self.is_file
|
||||
}
|
||||
|
||||
pub fn is_directory(&self) -> bool {
|
||||
!self.is_file
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal structure describing potentially partially-resident files and
|
||||
/// folders in the `Vfs`.
|
||||
pub enum VfsItem {
|
||||
File(VfsFile),
|
||||
Directory(VfsDirectory),
|
||||
}
|
||||
|
||||
impl VfsItem {
|
||||
fn path(&self) -> &Path {
|
||||
match self {
|
||||
VfsItem::File(file) => &file.path,
|
||||
VfsItem::Directory(dir) => &dir.path,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_from_type(kind: FileType, path: impl Into<PathBuf>) -> VfsItem {
|
||||
match kind {
|
||||
FileType::Directory => VfsItem::Directory(VfsDirectory {
|
||||
path: path.into(),
|
||||
children_enumerated: false,
|
||||
}),
|
||||
FileType::File => VfsItem::File(VfsFile {
|
||||
path: path.into(),
|
||||
contents: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VfsFile {
|
||||
pub(super) path: PathBuf,
|
||||
pub(super) contents: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
pub struct VfsDirectory {
|
||||
pub(super) path: PathBuf,
|
||||
pub(super) children_enumerated: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use maplit::hashmap;
|
||||
|
||||
use super::super::{error::FsErrorKind, event::VfsEvent, noop_fetcher::NoopFetcher};
|
||||
|
||||
#[test]
|
||||
fn from_snapshot_file() {
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let file = VfsSnapshot::file("hello, world!");
|
||||
|
||||
vfs.debug_load_snapshot("/hello.txt", file);
|
||||
|
||||
let entry = vfs.get_contents("/hello.txt").unwrap();
|
||||
assert_eq!(entry, b"hello, world!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_snapshot_dir() {
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
let dir = VfsSnapshot::dir(hashmap! {
|
||||
"a.txt" => VfsSnapshot::file("contents of a.txt"),
|
||||
"b.lua" => VfsSnapshot::file("contents of b.lua"),
|
||||
});
|
||||
|
||||
vfs.debug_load_snapshot("/dir", dir);
|
||||
|
||||
let children = vfs.get_children("/dir").unwrap();
|
||||
|
||||
let mut has_a = false;
|
||||
let mut has_b = false;
|
||||
|
||||
for child in children.into_iter() {
|
||||
if child.path() == Path::new("/dir/a.txt") {
|
||||
has_a = true;
|
||||
} else if child.path() == Path::new("/dir/b.lua") {
|
||||
has_b = true;
|
||||
} else {
|
||||
panic!("Unexpected child in /dir");
|
||||
}
|
||||
}
|
||||
|
||||
assert!(has_a, "/dir/a.txt was missing");
|
||||
assert!(has_b, "/dir/b.lua was missing");
|
||||
|
||||
let a = vfs.get_contents("/dir/a.txt").unwrap();
|
||||
assert_eq!(a, b"contents of a.txt");
|
||||
|
||||
let b = vfs.get_contents("/dir/b.lua").unwrap();
|
||||
assert_eq!(b, b"contents of b.lua");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changed_event() {
|
||||
#[derive(Default)]
|
||||
struct MockState {
|
||||
a_contents: &'static str,
|
||||
}
|
||||
|
||||
struct MockFetcher {
|
||||
inner: Rc<RefCell<MockState>>,
|
||||
}
|
||||
|
||||
impl VfsFetcher for MockFetcher {
|
||||
fn file_type(&mut self, path: &Path) -> io::Result<FileType> {
|
||||
if path == Path::new("/dir/a.txt") {
|
||||
return Ok(FileType::File);
|
||||
}
|
||||
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn read_contents(&mut self, path: &Path) -> io::Result<Vec<u8>> {
|
||||
if path == Path::new("/dir/a.txt") {
|
||||
let inner = self.inner.borrow();
|
||||
|
||||
return Ok(Vec::from(inner.a_contents));
|
||||
}
|
||||
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn read_children(&mut self, _path: &Path) -> io::Result<Vec<PathBuf>> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn create_directory(&mut self, _path: &Path) -> io::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn write_file(&mut self, _path: &Path, _contents: &[u8]) -> io::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn remove(&mut self, _path: &Path) -> io::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn watch(&mut self, _path: &Path) {}
|
||||
|
||||
fn unwatch(&mut self, _path: &Path) {}
|
||||
|
||||
fn receiver(&self) -> Receiver<VfsEvent> {
|
||||
crossbeam_channel::never()
|
||||
}
|
||||
}
|
||||
|
||||
let mock_state = Rc::new(RefCell::new(MockState {
|
||||
a_contents: "Initial contents",
|
||||
}));
|
||||
|
||||
let mut vfs = Vfs::new(MockFetcher {
|
||||
inner: mock_state.clone(),
|
||||
});
|
||||
|
||||
let a = vfs.get("/dir/a.txt").expect("mock file did not exist");
|
||||
|
||||
let contents = a.contents(&mut vfs).expect("mock file contents error");
|
||||
|
||||
assert_eq!(contents, b"Initial contents");
|
||||
|
||||
{
|
||||
let mut mock_state = mock_state.borrow_mut();
|
||||
mock_state.a_contents = "Changed contents";
|
||||
}
|
||||
|
||||
vfs.raise_file_changed("/dir/a.txt")
|
||||
.expect("error processing file change");
|
||||
|
||||
let contents = a.contents(&mut vfs).expect("mock file contents error");
|
||||
|
||||
assert_eq!(contents, b"Changed contents");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_event_existing() {
|
||||
let mut vfs = Vfs::new(NoopFetcher);
|
||||
|
||||
let file = VfsSnapshot::file("hello, world!");
|
||||
vfs.debug_load_snapshot("/hello.txt", file);
|
||||
|
||||
let hello = vfs.get("/hello.txt").expect("couldn't get hello.txt");
|
||||
|
||||
let contents = hello
|
||||
.contents(&mut vfs)
|
||||
.expect("couldn't get hello.txt contents");
|
||||
|
||||
assert_eq!(contents, b"hello, world!");
|
||||
|
||||
vfs.raise_file_removed("/hello.txt")
|
||||
.expect("error processing file removal");
|
||||
|
||||
match vfs.get("hello.txt") {
|
||||
Err(ref err) if err.kind() == FsErrorKind::NotFound => {}
|
||||
Ok(_) => {
|
||||
panic!("hello.txt was not removed from Vfs");
|
||||
}
|
||||
Err(err) => {
|
||||
panic!("Unexpected error: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user