forked from rojo-rbx/rojo
331 lines
8.7 KiB
Rust
331 lines
8.7 KiB
Rust
use std::{
|
|
cmp::Ordering,
|
|
collections::{HashMap, HashSet, BTreeSet},
|
|
fmt,
|
|
fs,
|
|
io,
|
|
path::{self, Path, PathBuf},
|
|
};
|
|
|
|
use failure::Fail;
|
|
use serde::{Serialize, Deserialize};
|
|
|
|
use crate::project::{Project, ProjectNode};
|
|
|
|
/// 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 {
|
|
fn new<P: Into<PathBuf>>(inner: io::Error, path: P) -> FsError {
|
|
FsError {
|
|
inner,
|
|
path: path.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for FsError {
|
|
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
|
|
write!(output, "{}: {}", self.path.display(), self.inner)
|
|
}
|
|
}
|
|
|
|
fn add_sync_points(imfs: &mut Imfs, node: &ProjectNode) -> Result<(), FsError> {
|
|
if let Some(path) = &node.path {
|
|
imfs.add_root(path)?;
|
|
}
|
|
|
|
for child in node.children.values() {
|
|
add_sync_points(imfs, child)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// The in-memory filesystem keeps a mirror of all files being watched by Rojo
|
|
/// in order to deduplicate file changes in the case of bidirectional syncing
|
|
/// from Roblox Studio.
|
|
///
|
|
/// It also enables Rojo to quickly generate React-like snapshots to make
|
|
/// reasoning about instances and how they relate to files easier.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Imfs {
|
|
items: HashMap<PathBuf, ImfsItem>,
|
|
roots: HashSet<PathBuf>,
|
|
}
|
|
|
|
impl Imfs {
|
|
pub fn new() -> Imfs {
|
|
Imfs {
|
|
items: HashMap::new(),
|
|
roots: HashSet::new(),
|
|
}
|
|
}
|
|
|
|
pub fn add_roots_from_project(&mut self, project: &Project) -> Result<(), FsError> {
|
|
add_sync_points(self, &project.tree)
|
|
}
|
|
|
|
pub fn get_roots(&self) -> &HashSet<PathBuf> {
|
|
&self.roots
|
|
}
|
|
|
|
pub fn get_items(&self) -> &HashMap<PathBuf, ImfsItem> {
|
|
&self.items
|
|
}
|
|
|
|
pub fn get(&self, path: &Path) -> Option<&ImfsItem> {
|
|
debug_assert!(path.is_absolute());
|
|
debug_assert!(self.is_within_roots(path));
|
|
|
|
self.items.get(path)
|
|
}
|
|
|
|
pub fn add_root(&mut self, path: &Path) -> Result<(), FsError> {
|
|
debug_assert!(path.is_absolute());
|
|
|
|
if !self.is_within_roots(path) {
|
|
self.roots.insert(path.to_path_buf());
|
|
self.descend_and_read_from_disk(path)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn remove_root(&mut self, path: &Path) {
|
|
debug_assert!(path.is_absolute());
|
|
|
|
if self.roots.get(path).is_some() {
|
|
self.remove_item(path);
|
|
|
|
if let Some(parent_path) = path.parent() {
|
|
self.unlink_child(parent_path, path);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn path_created(&mut self, path: &Path) -> Result<(), FsError> {
|
|
debug_assert!(path.is_absolute());
|
|
debug_assert!(self.is_within_roots(path));
|
|
|
|
self.descend_and_read_from_disk(path)
|
|
}
|
|
|
|
pub fn path_updated(&mut self, path: &Path) -> Result<(), FsError> {
|
|
debug_assert!(path.is_absolute());
|
|
debug_assert!(self.is_within_roots(path));
|
|
|
|
self.descend_and_read_from_disk(path)
|
|
}
|
|
|
|
pub fn path_removed(&mut self, path: &Path) -> Result<(), FsError> {
|
|
debug_assert!(path.is_absolute());
|
|
debug_assert!(self.is_within_roots(path));
|
|
|
|
self.remove_item(path);
|
|
|
|
if let Some(parent_path) = path.parent() {
|
|
self.unlink_child(parent_path, path);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn path_moved(&mut self, from_path: &Path, to_path: &Path) -> Result<(), FsError> {
|
|
self.path_removed(from_path)?;
|
|
self.path_created(to_path)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_root_for_path<'a>(&'a self, path: &Path) -> Option<&'a Path> {
|
|
for root_path in &self.roots {
|
|
if path.starts_with(root_path) {
|
|
return Some(root_path);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn remove_item(&mut self, path: &Path) {
|
|
if let Some(ImfsItem::Directory(directory)) = self.items.remove(path) {
|
|
for child_path in &directory.children {
|
|
self.remove_item(child_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn unlink_child(&mut self, parent: &Path, child: &Path) {
|
|
let parent_item = self.items.get_mut(parent);
|
|
|
|
match parent_item {
|
|
Some(ImfsItem::Directory(directory)) => {
|
|
directory.children.remove(child);
|
|
},
|
|
_ => {},
|
|
}
|
|
}
|
|
|
|
fn link_child(&mut self, parent: &Path, child: &Path) {
|
|
if self.is_within_roots(parent) {
|
|
let parent_item = self.items.get_mut(parent);
|
|
|
|
match parent_item {
|
|
Some(ImfsItem::Directory(directory)) => {
|
|
directory.children.insert(child.to_path_buf());
|
|
},
|
|
_ => {
|
|
panic!("Tried to link child of path that wasn't a directory!");
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn descend_and_read_from_disk(&mut self, path: &Path) -> Result<(), FsError> {
|
|
let root_path = self.get_root_path(path)
|
|
.expect("Tried to descent and read for path that wasn't within roots!");
|
|
|
|
// If this path is a root, we should read the entire thing.
|
|
if root_path == path {
|
|
self.read_from_disk(path)?;
|
|
return Ok(());
|
|
}
|
|
|
|
let relative_path = path.strip_prefix(root_path).unwrap();
|
|
let mut current_path = root_path.to_path_buf();
|
|
|
|
for component in relative_path.components() {
|
|
match component {
|
|
path::Component::Normal(name) => {
|
|
let next_path = current_path.join(name);
|
|
|
|
if self.items.contains_key(&next_path) {
|
|
current_path = next_path;
|
|
} else {
|
|
break;
|
|
}
|
|
},
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
self.read_from_disk(¤t_path)
|
|
}
|
|
|
|
fn read_from_disk(&mut self, path: &Path) -> Result<(), FsError> {
|
|
let metadata = fs::metadata(path)
|
|
.map_err(|e| FsError::new(e, path))?;
|
|
|
|
if metadata.is_file() {
|
|
let contents = fs::read(path)
|
|
.map_err(|e| FsError::new(e, path))?;
|
|
let item = ImfsItem::File(ImfsFile {
|
|
path: path.to_path_buf(),
|
|
contents,
|
|
});
|
|
|
|
self.items.insert(path.to_path_buf(), item);
|
|
|
|
if let Some(parent_path) = path.parent() {
|
|
self.link_child(parent_path, path);
|
|
}
|
|
|
|
Ok(())
|
|
} else if metadata.is_dir() {
|
|
let item = ImfsItem::Directory(ImfsDirectory {
|
|
path: path.to_path_buf(),
|
|
children: BTreeSet::new(),
|
|
});
|
|
|
|
self.items.insert(path.to_path_buf(), item);
|
|
|
|
let dir_children = fs::read_dir(path)
|
|
.map_err(|e| FsError::new(e, path))?;
|
|
|
|
for entry in dir_children {
|
|
let entry = entry
|
|
.map_err(|e| FsError::new(e, path))?;
|
|
|
|
let child_path = entry.path();
|
|
|
|
self.read_from_disk(&child_path)?;
|
|
}
|
|
|
|
if let Some(parent_path) = path.parent() {
|
|
self.link_child(parent_path, path);
|
|
}
|
|
|
|
Ok(())
|
|
} else {
|
|
panic!("Unexpected non-file, non-directory item");
|
|
}
|
|
}
|
|
|
|
fn get_root_path<'a>(&'a self, path: &Path) -> Option<&'a Path> {
|
|
for root_path in &self.roots {
|
|
if path.starts_with(root_path) {
|
|
return Some(root_path)
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn is_within_roots(&self, path: &Path) -> bool {
|
|
for root_path in &self.roots {
|
|
if path.starts_with(root_path) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ImfsFile {
|
|
pub path: PathBuf,
|
|
pub contents: Vec<u8>,
|
|
}
|
|
|
|
impl PartialOrd for ImfsFile {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for ImfsFile {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
self.path.cmp(&other.path)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ImfsDirectory {
|
|
pub path: PathBuf,
|
|
pub children: BTreeSet<PathBuf>,
|
|
}
|
|
|
|
impl PartialOrd for ImfsDirectory {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for ImfsDirectory {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
self.path.cmp(&other.path)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
|
pub enum ImfsItem {
|
|
File(ImfsFile),
|
|
Directory(ImfsDirectory),
|
|
} |