forked from rojo-rbx/rojo
Move memofs and rojo-insta-ext into crates folder
This commit is contained in:
18
crates/memofs/CHANGELOG.md
Normal file
18
crates/memofs/CHANGELOG.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# memofs Changelog
|
||||
|
||||
## Unreleased Changes
|
||||
|
||||
## 0.2.0 (2021-08-23)
|
||||
* Updated to `crossbeam-channel` 0.5.1.
|
||||
|
||||
## 0.1.3 (2020-11-19)
|
||||
* Added `set_watch_enabled` to `Vfs` and `VfsLock` to allow turning off file watching.
|
||||
|
||||
## 0.1.2 (2020-03-29)
|
||||
* `VfsSnapshot` now implements Serde's `Serialize` and `Deserialize` traits.
|
||||
|
||||
## 0.1.1 (2020-03-18)
|
||||
* Improved error messages using the [fs-err](https://crates.io/crates/fs-err) crate.
|
||||
|
||||
## 0.1.0 (2020-03-10)
|
||||
* Initial release
|
||||
17
crates/memofs/Cargo.toml
Normal file
17
crates/memofs/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "memofs"
|
||||
description = "Virtual filesystem with configurable backends."
|
||||
version = "0.2.0"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
homepage = "https://github.com/rojo-rbx/rojo/tree/master/memofs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
crossbeam-channel = "0.5.1"
|
||||
fs-err = "2.3.0"
|
||||
notify = "4.0.15"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
7
crates/memofs/LICENSE.txt
Normal file
7
crates/memofs/LICENSE.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Copyright 2020 The Rojo Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
22
crates/memofs/README.md
Normal file
22
crates/memofs/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# memofs
|
||||
[](https://crates.io/crates/memofs)
|
||||
|
||||
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)
|
||||
|
||||
## License
|
||||
memofs is available under the terms of the MIT license. See [LICENSE.txt](LICENSE.txt) or <https://opensource.org/licenses/MIT> for more details.
|
||||
7
crates/memofs/README.tpl
Normal file
7
crates/memofs/README.tpl
Normal file
@@ -0,0 +1,7 @@
|
||||
# {{crate}}
|
||||
[](https://crates.io/crates/memofs)
|
||||
|
||||
{{readme}}
|
||||
|
||||
## License
|
||||
memofs is available under the terms of the MIT license. See [LICENSE.txt](LICENSE.txt) or <https://opensource.org/licenses/MIT> for more details.
|
||||
249
crates/memofs/src/in_memory_fs.rs
Normal file
249
crates/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()),
|
||||
))
|
||||
}
|
||||
433
crates/memofs/src/lib.rs
Normal file
433
crates/memofs/src/lib.rs
Normal file
@@ -0,0 +1,433 @@
|
||||
/*!
|
||||
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>,
|
||||
watch_enabled: bool,
|
||||
}
|
||||
|
||||
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)?;
|
||||
|
||||
if self.watch_enabled {
|
||||
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)?;
|
||||
|
||||
if self.watch_enabled {
|
||||
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),
|
||||
watch_enabled: true,
|
||||
};
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Turns automatic file watching on or off. Enabled by default.
|
||||
///
|
||||
/// Turning off file watching may be useful for single-use cases, especially
|
||||
/// on platforms like macOS where registering file watches has significant
|
||||
/// performance cost.
|
||||
pub fn set_watch_enabled(&self, enabled: bool) {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.watch_enabled = enabled;
|
||||
}
|
||||
|
||||
/// 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<'_> {
|
||||
/// Turns automatic file watching on or off. Enabled by default.
|
||||
///
|
||||
/// Turning off file watching may be useful for single-use cases, especially
|
||||
/// on platforms like macOS where registering file watches has significant
|
||||
/// performance cost.
|
||||
pub fn set_watch_enabled(&mut self, enabled: bool) {
|
||||
self.inner.watch_enabled = enabled;
|
||||
}
|
||||
|
||||
/// 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
crates/memofs/src/noop_backend.rs
Normal file
76
crates/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",
|
||||
))
|
||||
}
|
||||
}
|
||||
45
crates/memofs/src/snapshot.rs
Normal file
45
crates/memofs/src/snapshot.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// A slice of a tree of files. Can be loaded into an
|
||||
/// [`InMemoryFs`](struct.InMemoryFs.html).
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
110
crates/memofs/src/std_backend.rs
Normal file
110
crates/memofs/src/std_backend.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
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_err::read(path)
|
||||
}
|
||||
|
||||
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()> {
|
||||
fs_err::write(path, data)
|
||||
}
|
||||
|
||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
||||
let entries: Result<Vec<_>, _> = fs_err::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_err::remove_file(path)
|
||||
}
|
||||
|
||||
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
||||
fs_err::remove_dir_all(path)
|
||||
}
|
||||
|
||||
fn metadata(&mut self, path: &Path) -> io::Result<Metadata> {
|
||||
let inner = fs_err::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))
|
||||
}
|
||||
}
|
||||
10
crates/rojo-insta-ext/Cargo.toml
Normal file
10
crates/rojo-insta-ext/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "rojo-insta-ext"
|
||||
version = "0.1.0"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0.99"
|
||||
serde_yaml = "0.8.9"
|
||||
2
crates/rojo-insta-ext/README.md
Normal file
2
crates/rojo-insta-ext/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Rojo Insta Extensions
|
||||
This crate has add-ons intended for use with Insta that are useful for snapshot tests.
|
||||
3
crates/rojo-insta-ext/src/lib.rs
Normal file
3
crates/rojo-insta-ext/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod redaction_map;
|
||||
|
||||
pub use redaction_map::*;
|
||||
85
crates/rojo-insta-ext/src/redaction_map.rs
Normal file
85
crates/rojo-insta-ext/src/redaction_map.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
/// Enables redacting any value that serializes as a string.
|
||||
///
|
||||
/// Used for transforming Rojo instance IDs into something deterministic.
|
||||
pub struct RedactionMap {
|
||||
ids: HashMap<String, usize>,
|
||||
last_id: usize,
|
||||
}
|
||||
|
||||
impl RedactionMap {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ids: HashMap::new(),
|
||||
last_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_redacted_value(&self, id: impl ToString) -> Option<String> {
|
||||
let id = id.to_string();
|
||||
|
||||
if self.ids.contains_key(&id) {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intern(&mut self, id: impl ToString) {
|
||||
let last_id = &mut self.last_id;
|
||||
|
||||
self.ids.entry(id.to_string()).or_insert_with(|| {
|
||||
*last_id += 1;
|
||||
*last_id
|
||||
});
|
||||
}
|
||||
|
||||
pub fn intern_iter<S: ToString>(&mut self, ids: impl Iterator<Item = S>) {
|
||||
for id in ids {
|
||||
self.intern(id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn redacted_yaml(&self, value: impl Serialize) -> serde_yaml::Value {
|
||||
let mut encoded = serde_yaml::to_value(value).expect("Couldn't encode value as YAML");
|
||||
|
||||
self.redact(&mut encoded);
|
||||
encoded
|
||||
}
|
||||
|
||||
pub fn redact(&self, yaml_value: &mut serde_yaml::Value) {
|
||||
use serde_yaml::{Mapping, Value};
|
||||
|
||||
match yaml_value {
|
||||
Value::String(value) => {
|
||||
if let Some(redacted) = self.ids.get(value) {
|
||||
*yaml_value = Value::String(format!("id-{}", *redacted));
|
||||
}
|
||||
}
|
||||
Value::Sequence(sequence) => {
|
||||
for value in sequence {
|
||||
self.redact(value);
|
||||
}
|
||||
}
|
||||
Value::Mapping(mapping) => {
|
||||
// We can't mutate the keys of a map in-place, so we take
|
||||
// ownership of the map and rebuild it.
|
||||
|
||||
let owned_map = std::mem::replace(mapping, Mapping::new());
|
||||
let mut new_map = Mapping::with_capacity(owned_map.len());
|
||||
|
||||
for (mut key, mut value) in owned_map {
|
||||
self.redact(&mut key);
|
||||
self.redact(&mut value);
|
||||
new_map.insert(key, value);
|
||||
}
|
||||
|
||||
*mapping = new_map;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user