forked from rojo-rbx/rojo
Reorganize some of the unwieldly parts of the codebase
* Moved commands into their own folder to reduce `bin.rs`'s size * Moved all of the VFS-related functionality into its own folder * Documented a lot of functions, including a few very obscure ones
This commit is contained in:
@@ -22,7 +22,7 @@ The `rojo serve` command uses three major components:
|
||||
### Transform Plugins
|
||||
Transform plugins (or filter plugins?) can interject in three places:
|
||||
* Transform a `VfsItem` that's being read into an `RbxInstance` in the VFS
|
||||
* Transform an `Rbxitem` that's being written into a `VfsItem` in the VFS
|
||||
* Transform an `RbxInstance` that's being written into a `VfsItem` in the VFS
|
||||
* Transform a file change into paths that need to be updated in the VFS watcher
|
||||
|
||||
The plan is to have several built-in plugins that can be rearranged/configured in project settings:
|
||||
|
||||
152
src/bin.rs
152
src/bin.rs
@@ -1,15 +1,7 @@
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
#[macro_use]
|
||||
extern crate rouille;
|
||||
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
#[macro_use] extern crate serde_derive;
|
||||
#[macro_use] extern crate rouille;
|
||||
#[macro_use] extern crate clap;
|
||||
#[macro_use] extern crate lazy_static;
|
||||
extern crate notify;
|
||||
extern crate rand;
|
||||
extern crate serde;
|
||||
@@ -21,22 +13,15 @@ pub mod core;
|
||||
pub mod project;
|
||||
pub mod pathext;
|
||||
pub mod vfs;
|
||||
pub mod vfs_watch;
|
||||
pub mod rbx;
|
||||
pub mod plugin;
|
||||
pub mod plugins;
|
||||
pub mod commands;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::process;
|
||||
|
||||
use core::Config;
|
||||
use pathext::canonicalish;
|
||||
use project::{Project, ProjectLoadError};
|
||||
use plugin::{PluginChain};
|
||||
use plugins::{DefaultPlugin, JsonModelPlugin, ScriptPlugin};
|
||||
use vfs::Vfs;
|
||||
use vfs_watch::VfsWatcher;
|
||||
|
||||
fn main() {
|
||||
let matches = clap_app!(rojo =>
|
||||
@@ -68,27 +53,13 @@ fn main() {
|
||||
_ => true,
|
||||
};
|
||||
|
||||
let server_id = rand::random::<u64>();
|
||||
|
||||
if verbose {
|
||||
println!("Server ID: {}", server_id);
|
||||
}
|
||||
|
||||
match matches.subcommand() {
|
||||
("init", sub_matches) => {
|
||||
let sub_matches = sub_matches.unwrap();
|
||||
let project_path = Path::new(sub_matches.value_of("PATH").unwrap_or("."));
|
||||
let full_path = canonicalish(project_path);
|
||||
|
||||
match Project::init(&full_path) {
|
||||
Ok(_) => {
|
||||
println!("Created new empty project at {}", full_path.display());
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create new project.\n{}", e);
|
||||
std::process::exit(1);
|
||||
},
|
||||
}
|
||||
commands::init(&full_path);
|
||||
},
|
||||
("serve", sub_matches) => {
|
||||
let sub_matches = sub_matches.unwrap();
|
||||
@@ -98,126 +69,29 @@ fn main() {
|
||||
None => std::env::current_dir().unwrap(),
|
||||
};
|
||||
|
||||
if verbose {
|
||||
println!("Attempting to locate project at {}", project_path.display());
|
||||
}
|
||||
|
||||
let project = match Project::load(&project_path) {
|
||||
Ok(v) => {
|
||||
println!("Using project from {}", project_path.display());
|
||||
v
|
||||
},
|
||||
Err(err) => {
|
||||
match err {
|
||||
ProjectLoadError::InvalidJson(serde_err) => {
|
||||
eprintln!(
|
||||
"Found invalid JSON!\nProject in: {}\nError: {}",
|
||||
project_path.display(),
|
||||
serde_err,
|
||||
);
|
||||
|
||||
std::process::exit(1);
|
||||
},
|
||||
ProjectLoadError::FailedToOpen | ProjectLoadError::FailedToRead => {
|
||||
eprintln!("Found project file, but failed to read it!");
|
||||
eprintln!(
|
||||
"Check the permissions of the project file at\n{}",
|
||||
project_path.display(),
|
||||
);
|
||||
|
||||
std::process::exit(1);
|
||||
},
|
||||
_ => {
|
||||
// Any other error is fine; use the default project.
|
||||
},
|
||||
}
|
||||
|
||||
println!("Found no project file, using default project...");
|
||||
Project::default()
|
||||
},
|
||||
};
|
||||
|
||||
let port = {
|
||||
match sub_matches.value_of("port") {
|
||||
Some(source) => match source.parse::<u64>() {
|
||||
Ok(value) => value,
|
||||
Ok(value) => Some(value),
|
||||
Err(_) => {
|
||||
eprintln!("Invalid port '{}'", source);
|
||||
std::process::exit(1);
|
||||
process::exit(1);
|
||||
},
|
||||
},
|
||||
None => project.serve_port,
|
||||
None => None,
|
||||
}
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
static ref PLUGIN_CHAIN: PluginChain = PluginChain::new(vec![
|
||||
Box::new(ScriptPlugin::new()),
|
||||
Box::new(JsonModelPlugin::new()),
|
||||
Box::new(DefaultPlugin::new()),
|
||||
]);
|
||||
}
|
||||
|
||||
let config = Config {
|
||||
port,
|
||||
verbose,
|
||||
server_id,
|
||||
};
|
||||
|
||||
if verbose {
|
||||
println!("Loading VFS...");
|
||||
}
|
||||
|
||||
let vfs = {
|
||||
let mut vfs = Vfs::new(config.clone(), &PLUGIN_CHAIN);
|
||||
|
||||
for (name, project_partition) in &project.partitions {
|
||||
let path = {
|
||||
let given_path = Path::new(&project_partition.path);
|
||||
|
||||
if given_path.is_absolute() {
|
||||
given_path.to_path_buf()
|
||||
} else {
|
||||
project_path.join(given_path)
|
||||
}
|
||||
};
|
||||
|
||||
if verbose {
|
||||
println!(
|
||||
"Partition '{}': {} @ {}",
|
||||
name,
|
||||
project_partition.target,
|
||||
project_partition.path
|
||||
);
|
||||
}
|
||||
|
||||
vfs.partitions.insert(name.clone(), path);
|
||||
}
|
||||
|
||||
Arc::new(Mutex::new(vfs))
|
||||
};
|
||||
|
||||
{
|
||||
let vfs = vfs.clone();
|
||||
let config = config.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
VfsWatcher::new(config, vfs).start();
|
||||
});
|
||||
}
|
||||
|
||||
println!("Server listening on port {}", port);
|
||||
|
||||
web::start(config.clone(), project.clone(), &PLUGIN_CHAIN, vfs.clone());
|
||||
commands::serve(&project_path, verbose, port);
|
||||
},
|
||||
("pack", _) => {
|
||||
eprintln!("'rojo pack' is not yet implemented!");
|
||||
std::process::exit(1);
|
||||
process::exit(1);
|
||||
},
|
||||
_ => {
|
||||
eprintln!("Please specify a subcommand!");
|
||||
eprintln!("Try 'rojo help' for information.");
|
||||
std::process::exit(1);
|
||||
process::exit(1);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
16
src/commands/init.rs
Normal file
16
src/commands/init.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
|
||||
use project::Project;
|
||||
|
||||
pub fn init(project_path: &PathBuf) {
|
||||
match Project::init(project_path) {
|
||||
Ok(_) => {
|
||||
println!("Created new empty project at {}", project_path.display());
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create new project.\n{}", e);
|
||||
process::exit(1);
|
||||
},
|
||||
}
|
||||
}
|
||||
5
src/commands/mod.rs
Normal file
5
src/commands/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod serve;
|
||||
mod init;
|
||||
|
||||
pub use self::serve::*;
|
||||
pub use self::init::*;
|
||||
114
src/commands/serve.rs
Normal file
114
src/commands/serve.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::process;
|
||||
|
||||
use rand;
|
||||
|
||||
use project::{Project, ProjectLoadError};
|
||||
use plugin::{PluginChain};
|
||||
use plugins::{DefaultPlugin, JsonModelPlugin, ScriptPlugin};
|
||||
use vfs::{VfsSession, VfsWatcher};
|
||||
use web;
|
||||
|
||||
pub fn serve(project_path: &PathBuf, verbose: bool, port: Option<u64>) {
|
||||
let server_id = rand::random::<u64>();
|
||||
|
||||
if verbose {
|
||||
println!("Attempting to locate project at {}", project_path.display());
|
||||
}
|
||||
|
||||
let project = match Project::load(project_path) {
|
||||
Ok(v) => {
|
||||
println!("Using project from {}", project_path.display());
|
||||
v
|
||||
},
|
||||
Err(err) => {
|
||||
match err {
|
||||
ProjectLoadError::InvalidJson(serde_err) => {
|
||||
eprintln!(
|
||||
"Found invalid JSON!\nProject in: {}\nError: {}",
|
||||
project_path.display(),
|
||||
serde_err,
|
||||
);
|
||||
|
||||
process::exit(1);
|
||||
},
|
||||
ProjectLoadError::FailedToOpen | ProjectLoadError::FailedToRead => {
|
||||
eprintln!("Found project file, but failed to read it!");
|
||||
eprintln!(
|
||||
"Check the permissions of the project file at\n{}",
|
||||
project_path.display(),
|
||||
);
|
||||
|
||||
process::exit(1);
|
||||
},
|
||||
_ => {
|
||||
// Any other error is fine; use the default project.
|
||||
},
|
||||
}
|
||||
|
||||
println!("Found no project file, using default project...");
|
||||
Project::default()
|
||||
},
|
||||
};
|
||||
|
||||
let web_config = web::WebConfig {
|
||||
verbose,
|
||||
port: port.unwrap_or(project.serve_port),
|
||||
server_id,
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
static ref PLUGIN_CHAIN: PluginChain = PluginChain::new(vec![
|
||||
Box::new(ScriptPlugin::new()),
|
||||
Box::new(JsonModelPlugin::new()),
|
||||
Box::new(DefaultPlugin::new()),
|
||||
]);
|
||||
}
|
||||
|
||||
if verbose {
|
||||
println!("Loading VFS...");
|
||||
}
|
||||
|
||||
let vfs = {
|
||||
let mut vfs = VfsSession::new(&PLUGIN_CHAIN);
|
||||
|
||||
for (name, project_partition) in &project.partitions {
|
||||
let path = {
|
||||
let given_path = Path::new(&project_partition.path);
|
||||
|
||||
if given_path.is_absolute() {
|
||||
given_path.to_path_buf()
|
||||
} else {
|
||||
project_path.join(given_path)
|
||||
}
|
||||
};
|
||||
|
||||
if verbose {
|
||||
println!(
|
||||
"Partition '{}': {} @ {}",
|
||||
name,
|
||||
project_partition.target,
|
||||
project_partition.path
|
||||
);
|
||||
}
|
||||
|
||||
vfs.partitions.insert(name.clone(), path);
|
||||
}
|
||||
|
||||
Arc::new(Mutex::new(vfs))
|
||||
};
|
||||
|
||||
{
|
||||
let vfs = vfs.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
VfsWatcher::new(vfs).start();
|
||||
});
|
||||
}
|
||||
|
||||
println!("Server listening on port {}", web_config.port);
|
||||
|
||||
web::start(web_config, project.clone(), &PLUGIN_CHAIN, vfs.clone());
|
||||
}
|
||||
@@ -1,8 +1 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub port: u64,
|
||||
pub verbose: bool,
|
||||
pub server_id: u64,
|
||||
}
|
||||
|
||||
pub type Route = Vec<String>;
|
||||
|
||||
@@ -2,15 +2,18 @@ use rbx::RbxInstance;
|
||||
use vfs::VfsItem;
|
||||
use core::Route;
|
||||
|
||||
// TODO: Add error case?
|
||||
pub enum TransformFileResult {
|
||||
Value(Option<RbxInstance>),
|
||||
Pass,
|
||||
|
||||
// TODO: Error case
|
||||
}
|
||||
|
||||
pub enum RbxChangeResult {
|
||||
Write(Option<VfsItem>),
|
||||
Pass,
|
||||
|
||||
// TODO: Error case
|
||||
}
|
||||
|
||||
pub enum FileChangeResult {
|
||||
@@ -19,11 +22,20 @@ pub enum FileChangeResult {
|
||||
}
|
||||
|
||||
pub trait Plugin {
|
||||
/// Invoked when a file is read from the filesystem and needs to be turned
|
||||
/// into a Roblox instance.
|
||||
fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult;
|
||||
|
||||
/// Invoked when a Roblox Instance change is reported by the Roblox Studio
|
||||
/// plugin and needs to be turned into a file to save.
|
||||
fn handle_rbx_change(&self, route: &Route, rbx_item: &RbxInstance) -> RbxChangeResult;
|
||||
|
||||
/// Invoked when a file changes on the filesystem. The result defines what
|
||||
/// routes are marked as needing to be refreshed.
|
||||
fn handle_file_change(&self, route: &Route) -> FileChangeResult;
|
||||
}
|
||||
|
||||
/// A set of plugins that are composed in order.
|
||||
pub struct PluginChain {
|
||||
plugins: Vec<Box<Plugin + Send + Sync>>,
|
||||
}
|
||||
|
||||
@@ -44,10 +44,19 @@ impl fmt::Display for ProjectInitError {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProjectPartition {
|
||||
/// A slash-separated path to a file or folder, relative to the project's
|
||||
/// directory.
|
||||
pub path: String,
|
||||
|
||||
/// A dot-separated route to a Roblox instance, relative to game.
|
||||
pub target: String,
|
||||
}
|
||||
|
||||
/// Represents a project configured by a user for use with Rojo. Holds anything
|
||||
/// that can be configured with `rojo.json`.
|
||||
///
|
||||
/// In the future, this object will hold dependency information and other handy
|
||||
/// configurables
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Project {
|
||||
@@ -57,6 +66,7 @@ pub struct Project {
|
||||
}
|
||||
|
||||
impl Project {
|
||||
/// Creates a new empty Project object with the given name.
|
||||
pub fn new<T: Into<String>>(name: T) -> Project {
|
||||
Project {
|
||||
name: name.into(),
|
||||
@@ -64,10 +74,12 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes a new project inside the given folder path.
|
||||
pub fn init<T: AsRef<Path>>(location: T) -> Result<Project, ProjectInitError> {
|
||||
let location = location.as_ref();
|
||||
let package_path = location.join(PROJECT_FILENAME);
|
||||
|
||||
// We abort if the project file already exists.
|
||||
match fs::metadata(&package_path) {
|
||||
Ok(_) => return Err(ProjectInitError::AlreadyExists),
|
||||
Err(_) => {},
|
||||
@@ -78,11 +90,14 @@ impl Project {
|
||||
Err(_) => return Err(ProjectInitError::FailedToCreate),
|
||||
};
|
||||
|
||||
// Try to give the project a meaningful name.
|
||||
// If we can't, we'll just fall back to a default.
|
||||
let name = match location.file_name() {
|
||||
Some(v) => v.to_string_lossy().into_owned(),
|
||||
None => "new-project".to_string(),
|
||||
};
|
||||
|
||||
// Configure the project with all of the values we know so far.
|
||||
let project = Project::new(name);
|
||||
let serialized = serde_json::to_string_pretty(&project).unwrap();
|
||||
|
||||
@@ -94,6 +109,8 @@ impl Project {
|
||||
Ok(project)
|
||||
}
|
||||
|
||||
/// Attempts to load a project from the file named PROJECT_FILENAME from the
|
||||
/// given folder.
|
||||
pub fn load<T: AsRef<Path>>(location: T) -> Result<Project, ProjectLoadError> {
|
||||
let package_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
|
||||
|
||||
@@ -120,6 +137,7 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves the given project file to the given folder with the appropriate name.
|
||||
pub fn save<T: AsRef<Path>>(&self, location: T) -> Result<(), ProjectSaveError> {
|
||||
let package_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
|
||||
|
||||
@@ -139,7 +157,7 @@ impl Project {
|
||||
impl Default for Project {
|
||||
fn default() -> Project {
|
||||
Project {
|
||||
name: "some-project".to_string(),
|
||||
name: "new-project".to_string(),
|
||||
serve_port: 8000,
|
||||
partitions: HashMap::new(),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Represents data about a Roblox instance
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RbxInstance {
|
||||
@@ -9,10 +10,14 @@ pub struct RbxInstance {
|
||||
pub properties: HashMap<String, RbxValue>,
|
||||
}
|
||||
|
||||
/// Any kind value that can be used by Roblox
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "type")]
|
||||
pub enum RbxValue {
|
||||
String {
|
||||
value: String,
|
||||
},
|
||||
|
||||
// TODO: Other primitives
|
||||
// TODO: Compound types like Vector3
|
||||
}
|
||||
|
||||
7
src/vfs/mod.rs
Normal file
7
src/vfs/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod vfs_session;
|
||||
mod vfs_item;
|
||||
mod vfs_watcher;
|
||||
|
||||
pub use self::vfs_session::*;
|
||||
pub use self::vfs_item::*;
|
||||
pub use self::vfs_watcher::*;
|
||||
31
src/vfs/vfs_item.rs
Normal file
31
src/vfs/vfs_item.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A VfsItem represents either a file or directory as it came from the filesystem.
|
||||
///
|
||||
/// The interface here is intentionally simplified to make it easier to traverse
|
||||
/// files that have been read into memory.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "type")]
|
||||
pub enum VfsItem {
|
||||
File {
|
||||
route: Vec<String>,
|
||||
contents: String,
|
||||
},
|
||||
Dir {
|
||||
route: Vec<String>,
|
||||
children: HashMap<String, VfsItem>,
|
||||
},
|
||||
}
|
||||
|
||||
impl VfsItem {
|
||||
pub fn name(&self) -> &String {
|
||||
self.route().last().unwrap()
|
||||
}
|
||||
|
||||
pub fn route(&self) -> &[String] {
|
||||
match self {
|
||||
&VfsItem::File { ref route, .. } => route,
|
||||
&VfsItem::Dir { ref route, .. } => route,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,14 @@ use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
|
||||
use core::Config;
|
||||
use plugin::PluginChain;
|
||||
use vfs::VfsItem;
|
||||
|
||||
/// Represents a virtual layer over multiple parts of the filesystem.
|
||||
///
|
||||
/// Paths in this system are represented as slices of strings, and are always
|
||||
/// relative to a partition, which is an absolute path into the real filesystem.
|
||||
pub struct Vfs {
|
||||
pub struct VfsSession {
|
||||
/// Contains all of the partitions mounted by the Vfs.
|
||||
///
|
||||
/// These must be absolute paths!
|
||||
@@ -25,8 +25,6 @@ pub struct Vfs {
|
||||
pub change_history: Vec<VfsChange>,
|
||||
|
||||
plugin_chain: &'static PluginChain,
|
||||
|
||||
config: Config,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -36,44 +34,13 @@ pub struct VfsChange {
|
||||
route: Vec<String>,
|
||||
}
|
||||
|
||||
/// A VfsItem represents either a file or directory as it came from the filesystem.
|
||||
///
|
||||
/// The interface here is intentionally simplified to make it easier to traverse
|
||||
/// files that have been read into memory.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "type")]
|
||||
pub enum VfsItem {
|
||||
File {
|
||||
route: Vec<String>,
|
||||
contents: String,
|
||||
},
|
||||
Dir {
|
||||
route: Vec<String>,
|
||||
children: HashMap<String, VfsItem>,
|
||||
},
|
||||
}
|
||||
|
||||
impl VfsItem {
|
||||
pub fn name(&self) -> &String {
|
||||
self.route().last().unwrap()
|
||||
}
|
||||
|
||||
pub fn route(&self) -> &[String] {
|
||||
match self {
|
||||
&VfsItem::File { ref route, .. } => route,
|
||||
&VfsItem::Dir { ref route, .. } => route,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Vfs {
|
||||
pub fn new(config: Config, plugin_chain: &'static PluginChain) -> Vfs {
|
||||
Vfs {
|
||||
impl VfsSession {
|
||||
pub fn new(plugin_chain: &'static PluginChain) -> VfsSession {
|
||||
VfsSession {
|
||||
partitions: HashMap::new(),
|
||||
start_time: Instant::now(),
|
||||
change_history: Vec::new(),
|
||||
plugin_chain,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,16 +152,8 @@ impl Vfs {
|
||||
/// Register a new change to the filesystem at the given timestamp and VFS
|
||||
/// route.
|
||||
pub fn add_change(&mut self, timestamp: f64, route: Vec<String>) {
|
||||
if self.config.verbose {
|
||||
println!("Received change {:?}, running through plugins...", route);
|
||||
}
|
||||
|
||||
match self.plugin_chain.handle_file_change(&route) {
|
||||
Some(routes) => {
|
||||
if self.config.verbose {
|
||||
println!("Adding changes from plugin: {:?}", routes);
|
||||
}
|
||||
|
||||
for route in routes {
|
||||
self.change_history.push(VfsChange {
|
||||
timestamp,
|
||||
@@ -4,24 +4,21 @@ use std::time::Duration;
|
||||
|
||||
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
|
||||
use core::Config;
|
||||
use pathext::path_to_route;
|
||||
use vfs::Vfs;
|
||||
use vfs::VfsSession;
|
||||
|
||||
/// An object that registers watchers on the real filesystem and relays those
|
||||
/// changes to the virtual filesystem layer.
|
||||
pub struct VfsWatcher {
|
||||
vfs: Arc<Mutex<Vfs>>,
|
||||
vfs: Arc<Mutex<VfsSession>>,
|
||||
watchers: Vec<RecommendedWatcher>,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl VfsWatcher {
|
||||
pub fn new(config: Config, vfs: Arc<Mutex<Vfs>>) -> VfsWatcher {
|
||||
pub fn new(vfs: Arc<Mutex<VfsSession>>) -> VfsWatcher {
|
||||
VfsWatcher {
|
||||
vfs,
|
||||
watchers: Vec::new(),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +42,6 @@ impl VfsWatcher {
|
||||
|
||||
{
|
||||
let vfs = self.vfs.clone();
|
||||
let config = self.config.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
@@ -53,10 +49,6 @@ impl VfsWatcher {
|
||||
let mut vfs = vfs.lock().unwrap();
|
||||
let current_time = vfs.current_time();
|
||||
|
||||
if config.verbose {
|
||||
println!("FS event {:?}", event);
|
||||
}
|
||||
|
||||
match event {
|
||||
DebouncedEvent::Write(ref change_path) |
|
||||
DebouncedEvent::Create(ref change_path) |
|
||||
41
src/web.rs
41
src/web.rs
@@ -5,14 +5,20 @@ use rouille;
|
||||
use serde;
|
||||
use serde_json;
|
||||
|
||||
use core::Config;
|
||||
use project::Project;
|
||||
use vfs::{Vfs, VfsChange};
|
||||
use vfs::{VfsSession, VfsChange};
|
||||
use rbx::RbxInstance;
|
||||
use plugin::PluginChain;
|
||||
|
||||
static MAX_BODY_SIZE: usize = 25 * 1024 * 1024; // 25 MiB
|
||||
|
||||
/// The set of configuration the web server needs to start.
|
||||
pub struct WebConfig {
|
||||
pub port: u64,
|
||||
pub verbose: bool,
|
||||
pub server_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ServerInfo<'a> {
|
||||
@@ -51,7 +57,12 @@ fn json<T: serde::Serialize>(value: T) -> rouille::Response {
|
||||
rouille::Response::from_data("application/json", data)
|
||||
}
|
||||
|
||||
/// Pulls text that may be JSON out of a Rouille Request object.
|
||||
///
|
||||
/// Doesn't do any actual parsing -- all this method does is verify the content
|
||||
/// type of the request and read the request's body.
|
||||
fn read_json_text(request: &rouille::Request) -> Option<String> {
|
||||
// Bail out if the request body isn't marked as JSON
|
||||
match request.header("Content-Type") {
|
||||
Some(header) => if !header.starts_with("application/json") {
|
||||
return None;
|
||||
@@ -64,14 +75,15 @@ fn read_json_text(request: &rouille::Request) -> Option<String> {
|
||||
None => return None,
|
||||
};
|
||||
|
||||
// Allocate a buffer and read up to MAX_BODY_SIZE+1 bytes into it.
|
||||
let mut out = Vec::new();
|
||||
match body.take(MAX_BODY_SIZE.saturating_add(1) as u64)
|
||||
.read_to_end(&mut out)
|
||||
{
|
||||
match body.take(MAX_BODY_SIZE.saturating_add(1) as u64).read_to_end(&mut out) {
|
||||
Ok(_) => {},
|
||||
Err(_) => return None,
|
||||
}
|
||||
|
||||
// If the body was too big (MAX_BODY_SIZE+1), we abort instead of trying to
|
||||
// process it.
|
||||
if out.len() > MAX_BODY_SIZE {
|
||||
return None;
|
||||
}
|
||||
@@ -84,6 +96,7 @@ fn read_json_text(request: &rouille::Request) -> Option<String> {
|
||||
Some(parsed)
|
||||
}
|
||||
|
||||
/// Reads the body out of a Rouille Request and attempts to turn it into JSON.
|
||||
fn read_json<T>(request: &rouille::Request) -> Option<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
@@ -98,10 +111,13 @@ where
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
// TODO: Change return type to some sort of Result
|
||||
|
||||
Some(parsed)
|
||||
}
|
||||
|
||||
pub fn start(config: Config, project: Project, plugin_chain: &'static PluginChain, vfs: Arc<Mutex<Vfs>>) {
|
||||
/// Start the Rojo web server and park our current thread.
|
||||
pub fn start(config: WebConfig, project: Project, plugin_chain: &'static PluginChain, vfs: Arc<Mutex<VfsSession>>) {
|
||||
let address = format!("localhost:{}", config.port);
|
||||
|
||||
let server_id = config.server_id.to_string();
|
||||
@@ -109,6 +125,8 @@ pub fn start(config: Config, project: Project, plugin_chain: &'static PluginChai
|
||||
rouille::start_server(address, move |request| {
|
||||
router!(request,
|
||||
(GET) (/) => {
|
||||
// Get a summary of information about the server.
|
||||
|
||||
let current_time = {
|
||||
let vfs = vfs.lock().unwrap();
|
||||
|
||||
@@ -125,6 +143,8 @@ pub fn start(config: Config, project: Project, plugin_chain: &'static PluginChai
|
||||
},
|
||||
|
||||
(GET) (/changes/{ last_time: f64 }) => {
|
||||
// Get the list of changes since the given time.
|
||||
|
||||
let vfs = vfs.lock().unwrap();
|
||||
let current_time = vfs.current_time();
|
||||
let changes = vfs.changes_since(last_time);
|
||||
@@ -137,11 +157,16 @@ pub fn start(config: Config, project: Project, plugin_chain: &'static PluginChai
|
||||
},
|
||||
|
||||
(POST) (/read) => {
|
||||
// Read some instances from the server according to a JSON
|
||||
// format body.
|
||||
|
||||
let read_request: Vec<Vec<String>> = match read_json(&request) {
|
||||
Some(v) => v,
|
||||
None => return rouille::Response::empty_400(),
|
||||
};
|
||||
|
||||
// Read the files off of the filesystem that the client
|
||||
// requested.
|
||||
let (items, current_time) = {
|
||||
let vfs = vfs.lock().unwrap();
|
||||
|
||||
@@ -159,6 +184,8 @@ pub fn start(config: Config, project: Project, plugin_chain: &'static PluginChai
|
||||
(items, current_time)
|
||||
};
|
||||
|
||||
// Transform all of our VfsItem objects into Roblox instances
|
||||
// the client can use.
|
||||
let rbx_items = items
|
||||
.iter()
|
||||
.map(|item| {
|
||||
@@ -182,6 +209,8 @@ pub fn start(config: Config, project: Project, plugin_chain: &'static PluginChai
|
||||
},
|
||||
|
||||
(POST) (/write) => {
|
||||
// Not yet implemented.
|
||||
|
||||
let _write_request: Vec<WriteSpecifier> = match read_json(&request) {
|
||||
Some(v) => v,
|
||||
None => return rouille::Response::empty_400(),
|
||||
|
||||
Reference in New Issue
Block a user