forked from rojo-rbx/rojo
Rename vfs -> memofs across the codebase
This commit is contained in:
249
memofs/src/in_memory_fs.rs
Normal file
249
memofs/src/in_memory_fs.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
use std::collections::{BTreeSet, HashMap, VecDeque};
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
|
||||
use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent, VfsSnapshot};
|
||||
|
||||
/// In-memory filesystem that can be used as a VFS backend.
|
||||
///
|
||||
/// Internally reference counted to enable giving a copy to
|
||||
/// [`Vfs`](struct.Vfs.html) and keeping the original to mutate the filesystem's
|
||||
/// state with.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InMemoryFs {
|
||||
inner: Arc<Mutex<InMemoryFsInner>>,
|
||||
}
|
||||
|
||||
impl InMemoryFs {
|
||||
/// Create a new empty `InMemoryFs`.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(InMemoryFsInner::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a [`VfsSnapshot`](enum.VfsSnapshot.html) into a subtree of the
|
||||
/// in-memory filesystem.
|
||||
///
|
||||
/// This function will return an error if the operations required to apply
|
||||
/// the snapshot result in errors, like trying to create a file inside a
|
||||
/// file.
|
||||
pub fn load_snapshot<P: Into<PathBuf>>(
|
||||
&mut self,
|
||||
path: P,
|
||||
snapshot: VfsSnapshot,
|
||||
) -> io::Result<()> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.load_snapshot(path.into(), snapshot)
|
||||
}
|
||||
|
||||
/// Raises a filesystem change event.
|
||||
///
|
||||
/// If this `InMemoryFs` is being used as the backend of a
|
||||
/// [`Vfs`](struct.Vfs.html), then any listeners be notified of this event.
|
||||
pub fn raise_event(&mut self, event: VfsEvent) {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
inner.event_sender.send(event).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InMemoryFsInner {
|
||||
entries: HashMap<PathBuf, Entry>,
|
||||
orphans: BTreeSet<PathBuf>,
|
||||
|
||||
event_receiver: Receiver<VfsEvent>,
|
||||
event_sender: Sender<VfsEvent>,
|
||||
}
|
||||
|
||||
impl InMemoryFsInner {
|
||||
fn new() -> Self {
|
||||
let (event_sender, event_receiver) = crossbeam_channel::unbounded();
|
||||
|
||||
Self {
|
||||
entries: HashMap::new(),
|
||||
orphans: BTreeSet::new(),
|
||||
event_receiver,
|
||||
event_sender,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_snapshot(&mut self, path: PathBuf, snapshot: VfsSnapshot) -> io::Result<()> {
|
||||
if let Some(parent_path) = path.parent() {
|
||||
if let Some(parent_entry) = self.entries.get_mut(parent_path) {
|
||||
if let Entry::Dir { children } = parent_entry {
|
||||
children.insert(path.clone());
|
||||
} else {
|
||||
return must_be_dir(parent_path);
|
||||
}
|
||||
} else {
|
||||
self.orphans.insert(path.clone());
|
||||
}
|
||||
} else {
|
||||
self.orphans.insert(path.clone());
|
||||
}
|
||||
|
||||
match snapshot {
|
||||
VfsSnapshot::File { contents } => {
|
||||
self.entries.insert(path, Entry::File { contents });
|
||||
}
|
||||
VfsSnapshot::Dir { children } => {
|
||||
self.entries.insert(
|
||||
path.clone(),
|
||||
Entry::Dir {
|
||||
children: BTreeSet::new(),
|
||||
},
|
||||
);
|
||||
|
||||
for (child_name, child) in children {
|
||||
let full_path = path.join(child_name);
|
||||
self.load_snapshot(full_path, child)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(&mut self, root_path: PathBuf) {
|
||||
self.orphans.remove(&root_path);
|
||||
|
||||
let mut to_remove = VecDeque::new();
|
||||
to_remove.push_back(root_path);
|
||||
|
||||
while let Some(path) = to_remove.pop_front() {
|
||||
if let Some(Entry::Dir { children }) = self.entries.remove(&path) {
|
||||
to_remove.extend(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Entry {
|
||||
File { contents: Vec<u8> },
|
||||
|
||||
Dir { children: BTreeSet<PathBuf> },
|
||||
}
|
||||
|
||||
impl VfsBackend for InMemoryFs {
|
||||
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>> {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
|
||||
match inner.entries.get(path) {
|
||||
Some(Entry::File { contents }) => Ok(contents.clone()),
|
||||
Some(Entry::Dir { .. }) => must_be_file(path),
|
||||
None => not_found(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
inner.load_snapshot(
|
||||
path.to_path_buf(),
|
||||
VfsSnapshot::File {
|
||||
contents: data.to_owned(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
|
||||
match inner.entries.get(path) {
|
||||
Some(Entry::Dir { children }) => {
|
||||
let iter = children
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|path| Ok(DirEntry { path }));
|
||||
|
||||
Ok(ReadDir {
|
||||
inner: Box::new(iter),
|
||||
})
|
||||
}
|
||||
Some(Entry::File { .. }) => must_be_dir(path),
|
||||
None => not_found(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
match inner.entries.get(path) {
|
||||
Some(Entry::File { .. }) => {
|
||||
inner.remove(path.to_owned());
|
||||
Ok(())
|
||||
}
|
||||
Some(Entry::Dir { .. }) => must_be_file(path),
|
||||
None => not_found(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
match inner.entries.get(path) {
|
||||
Some(Entry::Dir { .. }) => {
|
||||
inner.remove(path.to_owned());
|
||||
Ok(())
|
||||
}
|
||||
Some(Entry::File { .. }) => must_be_dir(path),
|
||||
None => not_found(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata(&mut self, path: &Path) -> io::Result<Metadata> {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
|
||||
match inner.entries.get(path) {
|
||||
Some(Entry::File { .. }) => Ok(Metadata { is_file: true }),
|
||||
Some(Entry::Dir { .. }) => Ok(Metadata { is_file: false }),
|
||||
None => not_found(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
|
||||
inner.event_receiver.clone()
|
||||
}
|
||||
|
||||
fn watch(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unwatch(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn must_be_file<T>(path: &Path) -> io::Result<T> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!(
|
||||
"path {} was a directory, but must be a file",
|
||||
path.display()
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
fn must_be_dir<T>(path: &Path) -> io::Result<T> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!(
|
||||
"path {} was a file, but must be a directory",
|
||||
path.display()
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
fn not_found<T>(path: &Path) -> io::Result<T> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("path {} not found", path.display()),
|
||||
))
|
||||
}
|
||||
404
memofs/src/lib.rs
Normal file
404
memofs/src/lib.rs
Normal file
@@ -0,0 +1,404 @@
|
||||
/*!
|
||||
Implementation of a virtual filesystem with a configurable backend and file
|
||||
watching.
|
||||
|
||||
memofs is currently an unstable minimum viable library. Its primary consumer is
|
||||
[Rojo](https://github.com/rojo-rbx/rojo), a build system for Roblox.
|
||||
|
||||
## Current Features
|
||||
* API similar to `std::fs`
|
||||
* Configurable backends
|
||||
* `StdBackend`, which uses `std::fs` and the `notify` crate
|
||||
* `NoopBackend`, which always throws errors
|
||||
* `InMemoryFs`, a simple in-memory filesystem useful for testing
|
||||
|
||||
## Future Features
|
||||
* Hash-based hierarchical memoization keys (hence the name)
|
||||
* Configurable caching (write-through, write-around, write-back)
|
||||
*/
|
||||
|
||||
mod in_memory_fs;
|
||||
mod noop_backend;
|
||||
mod snapshot;
|
||||
mod std_backend;
|
||||
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
|
||||
pub use in_memory_fs::InMemoryFs;
|
||||
pub use noop_backend::NoopBackend;
|
||||
pub use snapshot::VfsSnapshot;
|
||||
pub use std_backend::StdBackend;
|
||||
|
||||
mod sealed {
|
||||
use super::*;
|
||||
|
||||
/// Sealing trait for VfsBackend.
|
||||
pub trait Sealed {}
|
||||
|
||||
impl Sealed for NoopBackend {}
|
||||
impl Sealed for StdBackend {}
|
||||
impl Sealed for InMemoryFs {}
|
||||
}
|
||||
|
||||
/// Trait that transforms `io::Result<T>` into `io::Result<Option<T>>`.
|
||||
///
|
||||
/// `Ok(None)` takes the place of IO errors whose `io::ErrorKind` is `NotFound`.
|
||||
pub trait IoResultExt<T> {
|
||||
fn with_not_found(self) -> io::Result<Option<T>>;
|
||||
}
|
||||
|
||||
impl<T> IoResultExt<T> for io::Result<T> {
|
||||
fn with_not_found(self) -> io::Result<Option<T>> {
|
||||
match self {
|
||||
Ok(v) => Ok(Some(v)),
|
||||
Err(err) => {
|
||||
if err.kind() == io::ErrorKind::NotFound {
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Backend that can be used to create a `Vfs`.
|
||||
///
|
||||
/// This trait is sealed and cannot not be implemented outside this crate.
|
||||
pub trait VfsBackend: sealed::Sealed + Send + 'static {
|
||||
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>>;
|
||||
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()>;
|
||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir>;
|
||||
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 event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>;
|
||||
fn watch(&mut self, path: &Path) -> io::Result<()>;
|
||||
fn unwatch(&mut self, path: &Path) -> io::Result<()>;
|
||||
}
|
||||
|
||||
/// Vfs equivalent to [`std::fs::DirEntry`][std::fs::DirEntry].
|
||||
///
|
||||
/// [std::fs::DirEntry]: https://doc.rust-lang.org/stable/std/fs/struct.DirEntry.html
|
||||
pub struct DirEntry {
|
||||
pub(crate) path: PathBuf,
|
||||
}
|
||||
|
||||
impl DirEntry {
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
/// Vfs equivalent to [`std::fs::ReadDir`][std::fs::ReadDir].
|
||||
///
|
||||
/// [std::fs::ReadDir]: https://doc.rust-lang.org/stable/std/fs/struct.ReadDir.html
|
||||
pub struct ReadDir {
|
||||
pub(crate) inner: Box<dyn Iterator<Item = io::Result<DirEntry>>>,
|
||||
}
|
||||
|
||||
impl Iterator for ReadDir {
|
||||
type Item = io::Result<DirEntry>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next()
|
||||
}
|
||||
}
|
||||
|
||||
/// Vfs equivalent to [`std::fs::Metadata`][std::fs::Metadata].
|
||||
///
|
||||
/// [std::fs::Metadata]: https://doc.rust-lang.org/stable/std/fs/struct.Metadata.html
|
||||
#[derive(Debug)]
|
||||
pub struct Metadata {
|
||||
pub(crate) is_file: bool,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
pub fn is_file(&self) -> bool {
|
||||
self.is_file
|
||||
}
|
||||
|
||||
pub fn is_dir(&self) -> bool {
|
||||
!self.is_file
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an event that a filesystem can raise that might need to be
|
||||
/// handled.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum VfsEvent {
|
||||
Create(PathBuf),
|
||||
Write(PathBuf),
|
||||
Remove(PathBuf),
|
||||
}
|
||||
|
||||
/// Contains implementation details of the Vfs, wrapped by `Vfs` and `VfsLock`,
|
||||
/// the public interfaces to this type.
|
||||
struct VfsInner {
|
||||
backend: Box<dyn VfsBackend>,
|
||||
}
|
||||
|
||||
impl VfsInner {
|
||||
fn read<P: AsRef<Path>>(&mut self, path: P) -> io::Result<Arc<Vec<u8>>> {
|
||||
let path = path.as_ref();
|
||||
let contents = self.backend.read(path)?;
|
||||
self.backend.watch(path)?;
|
||||
Ok(Arc::new(contents))
|
||||
}
|
||||
|
||||
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&mut self, path: P, contents: C) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
let contents = contents.as_ref();
|
||||
self.backend.write(path, contents)
|
||||
}
|
||||
|
||||
fn read_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<ReadDir> {
|
||||
let path = path.as_ref();
|
||||
let dir = self.backend.read_dir(path)?;
|
||||
self.backend.watch(path)?;
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
fn remove_file<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
let _ = self.backend.unwatch(path);
|
||||
self.backend.remove_file(path)
|
||||
}
|
||||
|
||||
fn remove_dir_all<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
let _ = self.backend.unwatch(path);
|
||||
self.backend.remove_dir_all(path)
|
||||
}
|
||||
|
||||
fn metadata<P: AsRef<Path>>(&mut self, path: P) -> io::Result<Metadata> {
|
||||
let path = path.as_ref();
|
||||
self.backend.metadata(path)
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
self.backend.event_receiver()
|
||||
}
|
||||
|
||||
fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> {
|
||||
match event {
|
||||
VfsEvent::Remove(path) => {
|
||||
let _ = self.backend.unwatch(&path);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A virtual filesystem with a configurable backend.
|
||||
///
|
||||
/// All operations on the Vfs take a lock on an internal backend. For performing
|
||||
/// large batches of operations, it might be more performant to call `lock()`
|
||||
/// and use [`VfsLock`](struct.VfsLock.html) instead.
|
||||
pub struct Vfs {
|
||||
inner: Mutex<VfsInner>,
|
||||
}
|
||||
|
||||
impl Vfs {
|
||||
/// Creates a new `Vfs` with the default backend, `StdBackend`.
|
||||
pub fn new_default() -> Self {
|
||||
Self::new(StdBackend::new())
|
||||
}
|
||||
|
||||
/// Creates a new `Vfs` with the given backend.
|
||||
pub fn new<B: VfsBackend>(backend: B) -> Self {
|
||||
let lock = VfsInner {
|
||||
backend: Box::new(backend),
|
||||
};
|
||||
|
||||
Self {
|
||||
inner: Mutex::new(lock),
|
||||
}
|
||||
}
|
||||
|
||||
/// Manually lock the Vfs, useful for large batches of operations.
|
||||
pub fn lock(&self) -> VfsLock<'_> {
|
||||
VfsLock {
|
||||
inner: self.inner.lock().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a file from the VFS, or the underlying backend if it isn't
|
||||
/// resident.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::read`][std::fs::read].
|
||||
///
|
||||
/// [std::fs::read]: https://doc.rust-lang.org/stable/std/fs/fn.read.html
|
||||
#[inline]
|
||||
pub fn read<P: AsRef<Path>>(&self, path: P) -> io::Result<Arc<Vec<u8>>> {
|
||||
let path = path.as_ref();
|
||||
self.inner.lock().unwrap().read(path)
|
||||
}
|
||||
|
||||
/// Write a file to the VFS and the underlying backend.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::write`][std::fs::write].
|
||||
///
|
||||
/// [std::fs::write]: https://doc.rust-lang.org/stable/std/fs/fn.write.html
|
||||
#[inline]
|
||||
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&self, path: P, contents: C) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
let contents = contents.as_ref();
|
||||
self.inner.lock().unwrap().write(path, contents)
|
||||
}
|
||||
|
||||
/// Read all of the children of a directory.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::read_dir`][std::fs::read_dir].
|
||||
///
|
||||
/// [std::fs::read_dir]: https://doc.rust-lang.org/stable/std/fs/fn.read_dir.html
|
||||
#[inline]
|
||||
pub fn read_dir<P: AsRef<Path>>(&self, path: P) -> io::Result<ReadDir> {
|
||||
let path = path.as_ref();
|
||||
self.inner.lock().unwrap().read_dir(path)
|
||||
}
|
||||
|
||||
/// Remove a file.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
|
||||
///
|
||||
/// [std::fs::remove_file]: https://doc.rust-lang.org/stable/std/fs/fn.remove_file.html
|
||||
#[inline]
|
||||
pub fn remove_file<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
self.inner.lock().unwrap().remove_file(path)
|
||||
}
|
||||
|
||||
/// Remove a directory and all of its descendants.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::remove_dir_all`][std::fs::remove_dir_all].
|
||||
///
|
||||
/// [std::fs::remove_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.remove_dir_all.html
|
||||
#[inline]
|
||||
pub fn remove_dir_all<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
self.inner.lock().unwrap().remove_dir_all(path)
|
||||
}
|
||||
|
||||
/// Query metadata about the given path.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::metadata`][std::fs::metadata].
|
||||
///
|
||||
/// [std::fs::metadata]: https://doc.rust-lang.org/stable/std/fs/fn.metadata.html
|
||||
#[inline]
|
||||
pub fn metadata<P: AsRef<Path>>(&self, path: P) -> io::Result<Metadata> {
|
||||
let path = path.as_ref();
|
||||
self.inner.lock().unwrap().metadata(path)
|
||||
}
|
||||
|
||||
/// Retrieve a handle to the event receiver for this `Vfs`.
|
||||
#[inline]
|
||||
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
self.inner.lock().unwrap().event_receiver()
|
||||
}
|
||||
|
||||
/// Commit an event to this `Vfs`.
|
||||
#[inline]
|
||||
pub fn commit_event(&self, event: &VfsEvent) -> io::Result<()> {
|
||||
self.inner.lock().unwrap().commit_event(event)
|
||||
}
|
||||
}
|
||||
|
||||
/// A locked handle to a [`Vfs`](struct.Vfs.html), created by `Vfs::lock`.
|
||||
///
|
||||
/// Implements roughly the same API as [`Vfs`](struct.Vfs.html).
|
||||
pub struct VfsLock<'a> {
|
||||
inner: MutexGuard<'a, VfsInner>,
|
||||
}
|
||||
|
||||
impl VfsLock<'_> {
|
||||
/// Read a file from the VFS, or the underlying backend if it isn't
|
||||
/// resident.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::read`][std::fs::read].
|
||||
///
|
||||
/// [std::fs::read]: https://doc.rust-lang.org/stable/std/fs/fn.read.html
|
||||
#[inline]
|
||||
pub fn read<P: AsRef<Path>>(&mut self, path: P) -> io::Result<Arc<Vec<u8>>> {
|
||||
let path = path.as_ref();
|
||||
self.inner.read(path)
|
||||
}
|
||||
|
||||
/// Write a file to the VFS and the underlying backend.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::write`][std::fs::write].
|
||||
///
|
||||
/// [std::fs::write]: https://doc.rust-lang.org/stable/std/fs/fn.write.html
|
||||
#[inline]
|
||||
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(
|
||||
&mut self,
|
||||
path: P,
|
||||
contents: C,
|
||||
) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
let contents = contents.as_ref();
|
||||
self.inner.write(path, contents)
|
||||
}
|
||||
|
||||
/// Read all of the children of a directory.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::read_dir`][std::fs::read_dir].
|
||||
///
|
||||
/// [std::fs::read_dir]: https://doc.rust-lang.org/stable/std/fs/fn.read_dir.html
|
||||
#[inline]
|
||||
pub fn read_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<ReadDir> {
|
||||
let path = path.as_ref();
|
||||
self.inner.read_dir(path)
|
||||
}
|
||||
|
||||
/// Remove a file.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
|
||||
///
|
||||
/// [std::fs::remove_file]: https://doc.rust-lang.org/stable/std/fs/fn.remove_file.html
|
||||
#[inline]
|
||||
pub fn remove_file<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
self.inner.remove_file(path)
|
||||
}
|
||||
|
||||
/// Remove a directory and all of its descendants.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::remove_dir_all`][std::fs::remove_dir_all].
|
||||
///
|
||||
/// [std::fs::remove_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.remove_dir_all.html
|
||||
#[inline]
|
||||
pub fn remove_dir_all<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
self.inner.remove_dir_all(path)
|
||||
}
|
||||
|
||||
/// Query metadata about the given path.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::metadata`][std::fs::metadata].
|
||||
///
|
||||
/// [std::fs::metadata]: https://doc.rust-lang.org/stable/std/fs/fn.metadata.html
|
||||
#[inline]
|
||||
pub fn metadata<P: AsRef<Path>>(&mut self, path: P) -> io::Result<Metadata> {
|
||||
let path = path.as_ref();
|
||||
self.inner.metadata(path)
|
||||
}
|
||||
|
||||
/// Retrieve a handle to the event receiver for this `Vfs`.
|
||||
#[inline]
|
||||
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
self.inner.event_receiver()
|
||||
}
|
||||
|
||||
/// Commit an event to this `Vfs`.
|
||||
#[inline]
|
||||
pub fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> {
|
||||
self.inner.commit_event(event)
|
||||
}
|
||||
}
|
||||
76
memofs/src/noop_backend.rs
Normal file
76
memofs/src/noop_backend.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{Metadata, ReadDir, VfsBackend, VfsEvent};
|
||||
|
||||
/// `VfsBackend` that returns an error on every operation.
|
||||
#[non_exhaustive]
|
||||
pub struct NoopBackend;
|
||||
|
||||
impl NoopBackend {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl VfsBackend for NoopBackend {
|
||||
fn read(&mut self, _path: &Path) -> io::Result<Vec<u8>> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn write(&mut self, _path: &Path, _data: &[u8]) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn read_dir(&mut self, _path: &Path) -> io::Result<ReadDir> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn remove_file(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn remove_dir_all(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn metadata(&mut self, _path: &Path) -> io::Result<Metadata> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
crossbeam_channel::never()
|
||||
}
|
||||
|
||||
fn watch(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
|
||||
fn unwatch(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"NoopBackend doesn't do anything",
|
||||
))
|
||||
}
|
||||
}
|
||||
44
memofs/src/snapshot.rs
Normal file
44
memofs/src/snapshot.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// A slice of a tree of files. Can be loaded into an
|
||||
/// [`InMemoryFs`](struct.InMemoryFs.html).
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum VfsSnapshot {
|
||||
File {
|
||||
contents: Vec<u8>,
|
||||
},
|
||||
|
||||
Dir {
|
||||
children: BTreeMap<String, VfsSnapshot>,
|
||||
},
|
||||
}
|
||||
|
||||
impl VfsSnapshot {
|
||||
pub fn file<C: Into<Vec<u8>>>(contents: C) -> Self {
|
||||
Self::File {
|
||||
contents: contents.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dir<K: Into<String>, I: IntoIterator<Item = (K, VfsSnapshot)>>(children: I) -> Self {
|
||||
Self::Dir {
|
||||
children: children
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key.into(), value))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_file() -> Self {
|
||||
Self::File {
|
||||
contents: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_dir() -> Self {
|
||||
Self::Dir {
|
||||
children: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
111
memofs/src/std_backend.rs
Normal file
111
memofs/src/std_backend.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use notify::{watcher, DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
|
||||
use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent};
|
||||
|
||||
/// `VfsBackend` that uses `std::fs` and the `notify` crate.
|
||||
pub struct StdBackend {
|
||||
watcher: RecommendedWatcher,
|
||||
watcher_receiver: Receiver<VfsEvent>,
|
||||
}
|
||||
|
||||
impl StdBackend {
|
||||
pub fn new() -> StdBackend {
|
||||
let (notify_tx, notify_rx) = mpsc::channel();
|
||||
let watcher = watcher(notify_tx, Duration::from_millis(50)).unwrap();
|
||||
|
||||
let (tx, rx) = crossbeam_channel::unbounded();
|
||||
|
||||
thread::spawn(move || {
|
||||
for event in notify_rx {
|
||||
match event {
|
||||
DebouncedEvent::Create(path) => {
|
||||
tx.send(VfsEvent::Create(path))?;
|
||||
}
|
||||
DebouncedEvent::Write(path) => {
|
||||
tx.send(VfsEvent::Write(path))?;
|
||||
}
|
||||
DebouncedEvent::Remove(path) => {
|
||||
tx.send(VfsEvent::Remove(path))?;
|
||||
}
|
||||
DebouncedEvent::Rename(from, to) => {
|
||||
tx.send(VfsEvent::Remove(from))?;
|
||||
tx.send(VfsEvent::Create(to))?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Result::<(), crossbeam_channel::SendError<VfsEvent>>::Ok(())
|
||||
});
|
||||
|
||||
Self {
|
||||
watcher,
|
||||
watcher_receiver: rx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VfsBackend for StdBackend {
|
||||
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>> {
|
||||
fs::read(path)
|
||||
}
|
||||
|
||||
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> {
|
||||
fs::write(path, data)
|
||||
}
|
||||
|
||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
||||
let entries: Result<Vec<_>, _> = fs::read_dir(path)?.collect();
|
||||
let mut entries = entries?;
|
||||
|
||||
entries.sort_by_cached_key(|entry| entry.file_name());
|
||||
|
||||
let inner = entries
|
||||
.into_iter()
|
||||
.map(|entry| Ok(DirEntry { path: entry.path() }));
|
||||
|
||||
Ok(ReadDir {
|
||||
inner: Box::new(inner),
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
||||
fs::remove_file(path)
|
||||
}
|
||||
|
||||
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
||||
fs::remove_dir_all(path)
|
||||
}
|
||||
|
||||
fn metadata(&mut self, path: &Path) -> io::Result<Metadata> {
|
||||
let inner = fs::metadata(path)?;
|
||||
|
||||
Ok(Metadata {
|
||||
is_file: inner.is_file(),
|
||||
})
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
self.watcher_receiver.clone()
|
||||
}
|
||||
|
||||
fn watch(&mut self, path: &Path) -> io::Result<()> {
|
||||
self.watcher
|
||||
.watch(path, RecursiveMode::NonRecursive)
|
||||
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
||||
}
|
||||
|
||||
fn unwatch(&mut self, path: &Path) -> io::Result<()> {
|
||||
self.watcher
|
||||
.unwatch(path)
|
||||
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user