mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-21 05:06:29 +00:00
Merge plugin back into main repository (#49)
This commit is contained in:
committed by
GitHub
parent
c8f837d726
commit
6fa925a402
5
server/.editorconfig
Normal file
5
server/.editorconfig
Normal file
@@ -0,0 +1,5 @@
|
||||
[*.rs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
2
server/.gitignore
vendored
Normal file
2
server/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target/
|
||||
**/*.rs.bk
|
||||
1015
server/Cargo.lock
generated
Normal file
1015
server/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
server/Cargo.toml
Normal file
22
server/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "rojo"
|
||||
version = "0.4.0"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
description = "A tool to create robust Roblox projects"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/LPGhatguy/rojo"
|
||||
|
||||
[[bin]]
|
||||
name = "rojo"
|
||||
path = "src/bin.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.27.1"
|
||||
rouille = "1.0"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
notify = "4.0.0"
|
||||
rand = "0.3"
|
||||
regex = "0.2"
|
||||
lazy_static = "1.0"
|
||||
12
server/rustfmt.toml
Normal file
12
server/rustfmt.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
reorder_imports = true
|
||||
reorder_imported_names = true
|
||||
reorder_imports_in_group = true
|
||||
attributes_on_same_line_as_field = false
|
||||
attributes_on_same_line_as_variant = false
|
||||
chain_split_single_child = true
|
||||
wrap_comments = true
|
||||
imports_indent = "Block"
|
||||
match_block_trailing_comma = true
|
||||
match_pattern_separator_break_point = "Front"
|
||||
error_on_line_overflow = false
|
||||
struct_lit_multiline_style = "ForceMulti"
|
||||
97
server/src/bin.rs
Normal file
97
server/src/bin.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
#[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;
|
||||
extern crate serde_json;
|
||||
extern crate regex;
|
||||
|
||||
pub mod web;
|
||||
pub mod core;
|
||||
pub mod project;
|
||||
pub mod pathext;
|
||||
pub mod vfs;
|
||||
pub mod rbx;
|
||||
pub mod plugin;
|
||||
pub mod plugins;
|
||||
pub mod commands;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process;
|
||||
|
||||
use pathext::canonicalish;
|
||||
|
||||
fn main() {
|
||||
let matches = clap_app!(rojo =>
|
||||
(version: env!("CARGO_PKG_VERSION"))
|
||||
(author: env!("CARGO_PKG_AUTHORS"))
|
||||
(about: env!("CARGO_PKG_DESCRIPTION"))
|
||||
|
||||
(@subcommand init =>
|
||||
(about: "Creates a new Rojo project")
|
||||
(@arg PATH: "Path to the place to create the project. Defaults to the current directory.")
|
||||
)
|
||||
|
||||
(@subcommand serve =>
|
||||
(about: "Serves the project's files for use with the Rojo Studio plugin.")
|
||||
(@arg PROJECT: "Path to the project to serve. Defaults to the current directory.")
|
||||
(@arg port: --port +takes_value "The port to listen on. Defaults to 8000.")
|
||||
)
|
||||
|
||||
(@subcommand pack =>
|
||||
(about: "Packs the project into a GUI installer bundle. NOT YET IMPLEMENTED!")
|
||||
(@arg PROJECT: "Path to the project to pack. Defaults to the current directory.")
|
||||
)
|
||||
|
||||
(@arg verbose: --verbose "Enable extended logging.")
|
||||
).get_matches();
|
||||
|
||||
let verbose = match matches.occurrences_of("verbose") {
|
||||
0 => false,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
commands::init(&full_path);
|
||||
},
|
||||
("serve", sub_matches) => {
|
||||
let sub_matches = sub_matches.unwrap();
|
||||
|
||||
let project_path = match sub_matches.value_of("PROJECT") {
|
||||
Some(v) => canonicalish(PathBuf::from(v)),
|
||||
None => std::env::current_dir().unwrap(),
|
||||
};
|
||||
|
||||
let port = {
|
||||
match sub_matches.value_of("port") {
|
||||
Some(source) => match source.parse::<u64>() {
|
||||
Ok(value) => Some(value),
|
||||
Err(_) => {
|
||||
eprintln!("Invalid port '{}'", source);
|
||||
process::exit(1);
|
||||
},
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
};
|
||||
|
||||
commands::serve(&project_path, verbose, port);
|
||||
},
|
||||
("pack", _) => {
|
||||
eprintln!("'rojo pack' is not yet implemented!");
|
||||
process::exit(1);
|
||||
},
|
||||
_ => {
|
||||
eprintln!("Please specify a subcommand!");
|
||||
eprintln!("Try 'rojo help' for information.");
|
||||
process::exit(1);
|
||||
},
|
||||
}
|
||||
}
|
||||
16
server/src/commands/init.rs
Normal file
16
server/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
server/src/commands/mod.rs
Normal file
5
server/src/commands/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod serve;
|
||||
mod init;
|
||||
|
||||
pub use self::serve::*;
|
||||
pub use self::init::*;
|
||||
99
server/src/commands/serve.rs
Normal file
99
server/src/commands/serve.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
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()),
|
||||
]);
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
};
|
||||
|
||||
vfs.insert_partition(name, path);
|
||||
}
|
||||
|
||||
Arc::new(Mutex::new(vfs))
|
||||
};
|
||||
|
||||
println!("Server listening on port {}", web_config.port);
|
||||
|
||||
{
|
||||
let vfs = vfs.clone();
|
||||
thread::spawn(move || {
|
||||
VfsWatcher::new(vfs).start();
|
||||
});
|
||||
}
|
||||
|
||||
web::start(web_config, project.clone(), &PLUGIN_CHAIN, vfs.clone());
|
||||
}
|
||||
1
server/src/core.rs
Normal file
1
server/src/core.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub type Route = Vec<String>;
|
||||
120
server/src/pathext.rs
Normal file
120
server/src/pathext.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use std::env::current_dir;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
/// Converts a path to a 'route', used as the paths in Rojo.
|
||||
pub fn path_to_route<A, B>(root: A, value: B) -> Option<Vec<String>>
|
||||
where
|
||||
A: AsRef<Path>,
|
||||
B: AsRef<Path>,
|
||||
{
|
||||
let root = root.as_ref();
|
||||
let value = value.as_ref();
|
||||
|
||||
let relative = match value.strip_prefix(root) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
let result = relative
|
||||
.components()
|
||||
.map(|component| {
|
||||
component.as_os_str().to_string_lossy().into_owned()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Some(result)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_to_route() {
|
||||
fn t(root: &Path, value: &Path, result: Option<Vec<String>>) {
|
||||
assert_eq!(path_to_route(root, value), result);
|
||||
}
|
||||
|
||||
t(
|
||||
Path::new("/a/b/c"),
|
||||
Path::new("/a/b/c/d"),
|
||||
Some(vec!["d".to_string()]),
|
||||
);
|
||||
t(Path::new("/a/b"), Path::new("a"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn test_path_to_route_windows() {
|
||||
fn t(root: &Path, value: &Path, result: Option<Vec<String>>) {
|
||||
assert_eq!(path_to_route(root, value), result);
|
||||
}
|
||||
|
||||
t(
|
||||
Path::new("C:\\foo"),
|
||||
Path::new("C:\\foo\\bar\\baz"),
|
||||
Some(vec!["bar".to_string(), "baz".to_string()]),
|
||||
);
|
||||
}
|
||||
|
||||
/// Turns the path into an absolute one, using the current working directory if
|
||||
/// necessary.
|
||||
pub fn canonicalish<T: AsRef<Path>>(value: T) -> PathBuf {
|
||||
let cwd = current_dir().unwrap();
|
||||
|
||||
absoluteify(&cwd, value)
|
||||
}
|
||||
|
||||
/// Converts the given path to be absolute if it isn't already using a given
|
||||
/// root.
|
||||
pub fn absoluteify<A, B>(root: A, value: B) -> PathBuf
|
||||
where
|
||||
A: AsRef<Path>,
|
||||
B: AsRef<Path>,
|
||||
{
|
||||
let root = root.as_ref();
|
||||
let value = value.as_ref();
|
||||
|
||||
if value.is_absolute() {
|
||||
PathBuf::from(value)
|
||||
} else {
|
||||
root.join(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Collapses any `.` values along with any `..` values not at the start of the
|
||||
/// path.
|
||||
pub fn collapse<T: AsRef<Path>>(value: T) -> PathBuf {
|
||||
let value = value.as_ref();
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
for component in value.components() {
|
||||
match component {
|
||||
Component::ParentDir => match buffer.pop() {
|
||||
Some(_) => {},
|
||||
None => buffer.push(component.as_os_str()),
|
||||
},
|
||||
Component::CurDir => {},
|
||||
_ => {
|
||||
buffer.push(component.as_os_str());
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
buffer.iter().fold(PathBuf::new(), |mut acc, &x| {
|
||||
acc.push(x);
|
||||
acc
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collapse() {
|
||||
fn identity(buf: PathBuf) {
|
||||
assert_eq!(buf, collapse(&buf));
|
||||
}
|
||||
|
||||
identity(PathBuf::from("C:\\foo\\bar"));
|
||||
identity(PathBuf::from("/a/b/c"));
|
||||
identity(PathBuf::from("a/b"));
|
||||
|
||||
assert_eq!(collapse(PathBuf::from("a/b/..")), PathBuf::from("a"));
|
||||
assert_eq!(collapse(PathBuf::from("./a/b/c/..")), PathBuf::from("a/b"));
|
||||
assert_eq!(collapse(PathBuf::from("../a")), PathBuf::from("../a"));
|
||||
}
|
||||
82
server/src/plugin.rs
Normal file
82
server/src/plugin.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use rbx::RbxInstance;
|
||||
use vfs::VfsItem;
|
||||
use core::Route;
|
||||
|
||||
pub enum TransformFileResult {
|
||||
Value(Option<RbxInstance>),
|
||||
Pass,
|
||||
|
||||
// TODO: Error case
|
||||
}
|
||||
|
||||
pub enum RbxChangeResult {
|
||||
Write(Option<VfsItem>),
|
||||
Pass,
|
||||
|
||||
// TODO: Error case
|
||||
}
|
||||
|
||||
pub enum FileChangeResult {
|
||||
MarkChanged(Option<Vec<Route>>),
|
||||
Pass,
|
||||
}
|
||||
|
||||
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>>,
|
||||
}
|
||||
|
||||
impl PluginChain {
|
||||
pub fn new(plugins: Vec<Box<Plugin + Send + Sync>>) -> PluginChain {
|
||||
PluginChain {
|
||||
plugins,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transform_file(&self, vfs_item: &VfsItem) -> Option<RbxInstance> {
|
||||
for plugin in &self.plugins {
|
||||
match plugin.transform_file(self, vfs_item) {
|
||||
TransformFileResult::Value(rbx_item) => return rbx_item,
|
||||
TransformFileResult::Pass => {},
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn handle_rbx_change(&self, route: &Route, rbx_item: &RbxInstance) -> Option<VfsItem> {
|
||||
for plugin in &self.plugins {
|
||||
match plugin.handle_rbx_change(route, rbx_item) {
|
||||
RbxChangeResult::Write(vfs_item) => return vfs_item,
|
||||
RbxChangeResult::Pass => {},
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn handle_file_change(&self, route: &Route) -> Option<Vec<Route>> {
|
||||
for plugin in &self.plugins {
|
||||
match plugin.handle_file_change(route) {
|
||||
FileChangeResult::MarkChanged(changes) => return changes,
|
||||
FileChangeResult::Pass => {},
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
67
server/src/plugins/default_plugin.rs
Normal file
67
server/src/plugins/default_plugin.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use core::Route;
|
||||
use plugin::{Plugin, PluginChain, TransformFileResult, RbxChangeResult, FileChangeResult};
|
||||
use rbx::{RbxInstance, RbxValue};
|
||||
use vfs::VfsItem;
|
||||
|
||||
/// A plugin with simple transforms:
|
||||
/// * Directories become Folder instances
|
||||
/// * Files become StringValue objects with 'Value' as their contents
|
||||
pub struct DefaultPlugin;
|
||||
|
||||
impl DefaultPlugin {
|
||||
pub fn new() -> DefaultPlugin {
|
||||
DefaultPlugin
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for DefaultPlugin {
|
||||
fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult {
|
||||
match vfs_item {
|
||||
&VfsItem::File { ref contents, .. } => {
|
||||
let mut properties = HashMap::new();
|
||||
|
||||
properties.insert("Value".to_string(), RbxValue::String {
|
||||
value: contents.clone(),
|
||||
});
|
||||
|
||||
TransformFileResult::Value(Some(RbxInstance {
|
||||
name: vfs_item.name().clone(),
|
||||
class_name: "StringValue".to_string(),
|
||||
children: Vec::new(),
|
||||
properties,
|
||||
route: Some(vfs_item.route().to_vec()),
|
||||
}))
|
||||
},
|
||||
&VfsItem::Dir { ref children, .. } => {
|
||||
let mut rbx_children = Vec::new();
|
||||
|
||||
for (_, child_item) in children {
|
||||
match plugins.transform_file(child_item) {
|
||||
Some(rbx_item) => {
|
||||
rbx_children.push(rbx_item);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
TransformFileResult::Value(Some(RbxInstance {
|
||||
name: vfs_item.name().clone(),
|
||||
class_name: "*".to_string(),
|
||||
children: rbx_children,
|
||||
properties: HashMap::new(),
|
||||
route: Some(vfs_item.route().to_vec()),
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_file_change(&self, route: &Route) -> FileChangeResult {
|
||||
FileChangeResult::MarkChanged(Some(vec![route.clone()]))
|
||||
}
|
||||
|
||||
fn handle_rbx_change(&self, _route: &Route, _rbx_item: &RbxInstance) -> RbxChangeResult {
|
||||
RbxChangeResult::Pass
|
||||
}
|
||||
}
|
||||
55
server/src/plugins/json_model_plugin.rs
Normal file
55
server/src/plugins/json_model_plugin.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use regex::Regex;
|
||||
use serde_json;
|
||||
|
||||
use core::Route;
|
||||
use plugin::{Plugin, PluginChain, TransformFileResult, RbxChangeResult, FileChangeResult};
|
||||
use rbx::RbxInstance;
|
||||
use vfs::VfsItem;
|
||||
|
||||
lazy_static! {
|
||||
static ref JSON_MODEL_PATTERN: Regex = Regex::new(r"^(.*?)\.model\.json$").unwrap();
|
||||
}
|
||||
|
||||
pub struct JsonModelPlugin;
|
||||
|
||||
impl JsonModelPlugin {
|
||||
pub fn new() -> JsonModelPlugin {
|
||||
JsonModelPlugin
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for JsonModelPlugin {
|
||||
fn transform_file(&self, _plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult {
|
||||
match vfs_item {
|
||||
&VfsItem::File { ref contents, .. } => {
|
||||
let rbx_name = match JSON_MODEL_PATTERN.captures(vfs_item.name()) {
|
||||
Some(captures) => captures.get(1).unwrap().as_str().to_string(),
|
||||
None => return TransformFileResult::Pass,
|
||||
};
|
||||
|
||||
let mut rbx_item: RbxInstance = match serde_json::from_str(contents) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("Unable to parse JSON Model File named {}: {}", vfs_item.name(), e);
|
||||
|
||||
return TransformFileResult::Pass; // This should be an error in the future!
|
||||
},
|
||||
};
|
||||
|
||||
rbx_item.route = Some(vfs_item.route().to_vec());
|
||||
rbx_item.name = rbx_name;
|
||||
|
||||
TransformFileResult::Value(Some(rbx_item))
|
||||
},
|
||||
&VfsItem::Dir { .. } => TransformFileResult::Pass,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_file_change(&self, _route: &Route) -> FileChangeResult {
|
||||
FileChangeResult::Pass
|
||||
}
|
||||
|
||||
fn handle_rbx_change(&self, _route: &Route, _rbx_item: &RbxInstance) -> RbxChangeResult {
|
||||
RbxChangeResult::Pass
|
||||
}
|
||||
}
|
||||
7
server/src/plugins/mod.rs
Normal file
7
server/src/plugins/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod default_plugin;
|
||||
mod script_plugin;
|
||||
mod json_model_plugin;
|
||||
|
||||
pub use self::default_plugin::*;
|
||||
pub use self::script_plugin::*;
|
||||
pub use self::json_model_plugin::*;
|
||||
124
server/src/plugins/script_plugin.rs
Normal file
124
server/src/plugins/script_plugin.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use core::Route;
|
||||
use plugin::{Plugin, PluginChain, TransformFileResult, RbxChangeResult, FileChangeResult};
|
||||
use rbx::{RbxInstance, RbxValue};
|
||||
use vfs::VfsItem;
|
||||
|
||||
lazy_static! {
|
||||
static ref SERVER_PATTERN: Regex = Regex::new(r"^(.*?)\.server\.lua$").unwrap();
|
||||
static ref CLIENT_PATTERN: Regex = Regex::new(r"^(.*?)\.client\.lua$").unwrap();
|
||||
static ref MODULE_PATTERN: Regex = Regex::new(r"^(.*?)\.lua$").unwrap();
|
||||
}
|
||||
|
||||
static SERVER_INIT: &'static str = "init.server.lua";
|
||||
static CLIENT_INIT: &'static str = "init.client.lua";
|
||||
static MODULE_INIT: &'static str = "init.lua";
|
||||
|
||||
pub struct ScriptPlugin;
|
||||
|
||||
impl ScriptPlugin {
|
||||
pub fn new() -> ScriptPlugin {
|
||||
ScriptPlugin
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for ScriptPlugin {
|
||||
fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult {
|
||||
match vfs_item {
|
||||
&VfsItem::File { ref contents, .. } => {
|
||||
let name = vfs_item.name();
|
||||
|
||||
let (class_name, rbx_name) = {
|
||||
if let Some(captures) = SERVER_PATTERN.captures(name) {
|
||||
("Script".to_string(), captures.get(1).unwrap().as_str().to_string())
|
||||
} else if let Some(captures) = CLIENT_PATTERN.captures(name) {
|
||||
("LocalScript".to_string(), captures.get(1).unwrap().as_str().to_string())
|
||||
} else if let Some(captures) = MODULE_PATTERN.captures(name) {
|
||||
("ModuleScript".to_string(), captures.get(1).unwrap().as_str().to_string())
|
||||
} else {
|
||||
return TransformFileResult::Pass;
|
||||
}
|
||||
};
|
||||
|
||||
let mut properties = HashMap::new();
|
||||
|
||||
properties.insert("Source".to_string(), RbxValue::String {
|
||||
value: contents.clone(),
|
||||
});
|
||||
|
||||
TransformFileResult::Value(Some(RbxInstance {
|
||||
name: rbx_name,
|
||||
class_name: class_name,
|
||||
children: Vec::new(),
|
||||
properties,
|
||||
route: Some(vfs_item.route().to_vec()),
|
||||
}))
|
||||
},
|
||||
&VfsItem::Dir { ref children, .. } => {
|
||||
let init_item = {
|
||||
let maybe_item = children.get(SERVER_INIT)
|
||||
.or(children.get(CLIENT_INIT))
|
||||
.or(children.get(MODULE_INIT));
|
||||
|
||||
match maybe_item {
|
||||
Some(v) => v,
|
||||
None => return TransformFileResult::Pass,
|
||||
}
|
||||
};
|
||||
|
||||
let mut rbx_item = match self.transform_file(plugins, init_item) {
|
||||
TransformFileResult::Value(Some(item)) => item,
|
||||
_ => {
|
||||
eprintln!("Inconsistency detected in ScriptPlugin!");
|
||||
return TransformFileResult::Pass;
|
||||
},
|
||||
};
|
||||
|
||||
rbx_item.name.clear();
|
||||
rbx_item.name.push_str(vfs_item.name());
|
||||
|
||||
for (child_name, child_item) in children {
|
||||
if child_name == init_item.name() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match plugins.transform_file(child_item) {
|
||||
Some(child_rbx_item) => {
|
||||
rbx_item.children.push(child_rbx_item);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
TransformFileResult::Value(Some(rbx_item))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_file_change(&self, route: &Route) -> FileChangeResult {
|
||||
let leaf = match route.last() {
|
||||
Some(v) => v,
|
||||
None => return FileChangeResult::Pass,
|
||||
};
|
||||
|
||||
let is_init = leaf == SERVER_INIT
|
||||
|| leaf == CLIENT_INIT
|
||||
|| leaf == MODULE_INIT;
|
||||
|
||||
if is_init {
|
||||
let mut changed = route.clone();
|
||||
changed.pop();
|
||||
|
||||
FileChangeResult::MarkChanged(Some(vec![changed]))
|
||||
} else {
|
||||
FileChangeResult::Pass
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_rbx_change(&self, _route: &Route, _rbx_item: &RbxInstance) -> RbxChangeResult {
|
||||
RbxChangeResult::Pass
|
||||
}
|
||||
}
|
||||
165
server/src/project.rs
Normal file
165
server/src/project.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use serde_json;
|
||||
|
||||
pub static PROJECT_FILENAME: &'static str = "rojo.json";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProjectLoadError {
|
||||
DidNotExist,
|
||||
FailedToOpen,
|
||||
FailedToRead,
|
||||
InvalidJson(serde_json::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProjectSaveError {
|
||||
FailedToCreate,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProjectInitError {
|
||||
AlreadyExists,
|
||||
FailedToCreate,
|
||||
FailedToWrite,
|
||||
}
|
||||
|
||||
impl fmt::Display for ProjectInitError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
&ProjectInitError::AlreadyExists => {
|
||||
write!(f, "A project already exists at that location.")
|
||||
},
|
||||
&ProjectInitError::FailedToCreate | &ProjectInitError::FailedToWrite => {
|
||||
write!(f, "Failed to write to the given location.")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
pub name: String,
|
||||
pub serve_port: u64,
|
||||
pub partitions: HashMap<String, ProjectPartition>,
|
||||
}
|
||||
|
||||
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(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(_) => {},
|
||||
}
|
||||
|
||||
let mut file = match File::create(&package_path) {
|
||||
Ok(f) => f,
|
||||
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();
|
||||
|
||||
match file.write(serialized.as_bytes()) {
|
||||
Ok(_) => {},
|
||||
Err(_) => return Err(ProjectInitError::FailedToWrite),
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
match fs::metadata(&package_path) {
|
||||
Ok(_) => {},
|
||||
Err(_) => return Err(ProjectLoadError::DidNotExist),
|
||||
}
|
||||
|
||||
let mut file = match File::open(&package_path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return Err(ProjectLoadError::FailedToOpen),
|
||||
};
|
||||
|
||||
let mut contents = String::new();
|
||||
|
||||
match file.read_to_string(&mut contents) {
|
||||
Ok(_) => {},
|
||||
Err(_) => return Err(ProjectLoadError::FailedToRead),
|
||||
}
|
||||
|
||||
match serde_json::from_str(&contents) {
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => return Err(ProjectLoadError::InvalidJson(e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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));
|
||||
|
||||
let mut file = match File::create(&package_path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return Err(ProjectSaveError::FailedToCreate),
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_string_pretty(self).unwrap();
|
||||
|
||||
file.write(serialized.as_bytes()).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Project {
|
||||
fn default() -> Project {
|
||||
Project {
|
||||
name: "new-project".to_string(),
|
||||
serve_port: 8000,
|
||||
partitions: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
34
server/src/rbx.rs
Normal file
34
server/src/rbx.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Represents data about a Roblox instance
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct RbxInstance {
|
||||
pub name: String,
|
||||
pub class_name: String,
|
||||
pub children: Vec<RbxInstance>,
|
||||
pub properties: HashMap<String, RbxValue>,
|
||||
|
||||
/// The route that this instance was generated from, if there was one.
|
||||
pub route: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Any kind value that can be used by Roblox
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase", tag = "Type")]
|
||||
pub enum RbxValue {
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
String {
|
||||
value: String,
|
||||
},
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
Bool {
|
||||
value: bool,
|
||||
},
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
Number {
|
||||
value: f64,
|
||||
},
|
||||
|
||||
// TODO: Compound types like Vector3
|
||||
}
|
||||
7
server/src/vfs/mod.rs
Normal file
7
server/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
server/src/vfs/vfs_item.rs
Normal file
31
server/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,
|
||||
}
|
||||
}
|
||||
}
|
||||
214
server/src/vfs/vfs_session.rs
Normal file
214
server/src/vfs/vfs_session.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
|
||||
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 VfsSession {
|
||||
/// Contains all of the partitions mounted by the Vfs.
|
||||
///
|
||||
/// These must be absolute paths!
|
||||
partitions: HashMap<String, PathBuf>,
|
||||
|
||||
/// A chronologically-sorted list of routes that changed since the Vfs was
|
||||
/// created, along with a timestamp denoting when.
|
||||
change_history: Vec<VfsChange>,
|
||||
|
||||
/// When the Vfs was initialized; used for change tracking.
|
||||
start_time: Instant,
|
||||
|
||||
plugin_chain: &'static PluginChain,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VfsChange {
|
||||
timestamp: f64,
|
||||
route: Vec<String>,
|
||||
}
|
||||
|
||||
impl VfsSession {
|
||||
pub fn new(plugin_chain: &'static PluginChain) -> VfsSession {
|
||||
VfsSession {
|
||||
partitions: HashMap::new(),
|
||||
start_time: Instant::now(),
|
||||
change_history: Vec::new(),
|
||||
plugin_chain,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_partitions(&self) -> &HashMap<String, PathBuf> {
|
||||
&self.partitions
|
||||
}
|
||||
|
||||
pub fn insert_partition<P: Into<PathBuf>>(&mut self, name: &str, path: P) {
|
||||
let path = path.into();
|
||||
|
||||
assert!(path.is_absolute());
|
||||
|
||||
self.partitions.insert(name.to_string(), path.into());
|
||||
}
|
||||
|
||||
fn route_to_path(&self, route: &[String]) -> Option<PathBuf> {
|
||||
let (partition_name, rest) = match route.split_first() {
|
||||
Some((first, rest)) => (first, rest),
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let partition = match self.partitions.get(partition_name) {
|
||||
Some(v) => v,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
// It's possible that the partition points to a file if `rest` is empty.
|
||||
// Joining "" onto a path will put a trailing slash on, which causes
|
||||
// file reads to fail.
|
||||
let full_path = if rest.is_empty() {
|
||||
partition.clone()
|
||||
} else {
|
||||
let joined = rest.join("/");
|
||||
let relative = Path::new(&joined);
|
||||
|
||||
partition.join(relative)
|
||||
};
|
||||
|
||||
Some(full_path)
|
||||
}
|
||||
|
||||
fn read_dir<P: AsRef<Path>>(&self, route: &[String], path: P) -> Result<VfsItem, ()> {
|
||||
let path = path.as_ref();
|
||||
let reader = match fs::read_dir(path) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Err(()),
|
||||
};
|
||||
|
||||
let mut children = HashMap::new();
|
||||
|
||||
for entry in reader {
|
||||
let entry = match entry {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Err(()),
|
||||
};
|
||||
|
||||
let path = entry.path();
|
||||
let name = path.file_name().unwrap().to_string_lossy().into_owned();
|
||||
|
||||
let mut child_route = route.iter().cloned().collect::<Vec<_>>();
|
||||
child_route.push(name.clone());
|
||||
|
||||
match self.read_path(&child_route, &path) {
|
||||
Ok(child_item) => {
|
||||
children.insert(name, child_item);
|
||||
},
|
||||
Err(_) => {},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(VfsItem::Dir {
|
||||
route: route.iter().cloned().collect::<Vec<_>>(),
|
||||
children,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_file<P: AsRef<Path>>(&self, route: &[String], path: P) -> Result<VfsItem, ()> {
|
||||
let path = path.as_ref();
|
||||
let mut file = match File::open(path) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Err(()),
|
||||
};
|
||||
|
||||
let mut contents = String::new();
|
||||
|
||||
match file.read_to_string(&mut contents) {
|
||||
Ok(_) => {},
|
||||
Err(_) => return Err(()),
|
||||
}
|
||||
|
||||
Ok(VfsItem::File {
|
||||
route: route.iter().cloned().collect::<Vec<_>>(),
|
||||
contents,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_path<P: AsRef<Path>>(&self, route: &[String], path: P) -> Result<VfsItem, ()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let metadata = match fs::metadata(path) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Err(()),
|
||||
};
|
||||
|
||||
if metadata.is_dir() {
|
||||
self.read_dir(route, path)
|
||||
} else if metadata.is_file() {
|
||||
self.read_file(route, path)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current time, used for logging timestamps for file changes.
|
||||
pub fn current_time(&self) -> f64 {
|
||||
let elapsed = self.start_time.elapsed();
|
||||
|
||||
elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 * 1e-9
|
||||
}
|
||||
|
||||
/// 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>) {
|
||||
match self.plugin_chain.handle_file_change(&route) {
|
||||
Some(routes) => {
|
||||
for route in routes {
|
||||
self.change_history.push(VfsChange {
|
||||
timestamp,
|
||||
route,
|
||||
});
|
||||
}
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect a list of changes that occured since the given timestamp.
|
||||
pub fn changes_since(&self, timestamp: f64) -> &[VfsChange] {
|
||||
let mut marker: Option<usize> = None;
|
||||
|
||||
for (index, value) in self.change_history.iter().enumerate().rev() {
|
||||
if value.timestamp >= timestamp {
|
||||
marker = Some(index);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(index) = marker {
|
||||
&self.change_history[index..]
|
||||
} else {
|
||||
&self.change_history[..0]
|
||||
}
|
||||
}
|
||||
|
||||
/// Read an item from the filesystem using the given VFS route.
|
||||
pub fn read(&self, route: &[String]) -> Result<VfsItem, ()> {
|
||||
match self.route_to_path(route) {
|
||||
Some(path) => self.read_path(route, &path),
|
||||
None => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&self, _route: &[String], _item: VfsItem) -> Result<(), ()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn delete(&self, _route: &[String]) -> Result<(), ()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
108
server/src/vfs/vfs_watcher.rs
Normal file
108
server/src/vfs/vfs_watcher.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
|
||||
use pathext::path_to_route;
|
||||
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<VfsSession>>,
|
||||
}
|
||||
|
||||
impl VfsWatcher {
|
||||
pub fn new(vfs: Arc<Mutex<VfsSession>>) -> VfsWatcher {
|
||||
VfsWatcher {
|
||||
vfs,
|
||||
}
|
||||
}
|
||||
|
||||
fn start_watcher(
|
||||
vfs: Arc<Mutex<VfsSession>>,
|
||||
rx: mpsc::Receiver<DebouncedEvent>,
|
||||
partition_name: String,
|
||||
root_path: PathBuf,
|
||||
) {
|
||||
loop {
|
||||
let event = rx.recv().unwrap();
|
||||
|
||||
let mut vfs = vfs.lock().unwrap();
|
||||
let current_time = vfs.current_time();
|
||||
|
||||
match event {
|
||||
DebouncedEvent::Write(ref change_path) |
|
||||
DebouncedEvent::Create(ref change_path) |
|
||||
DebouncedEvent::Remove(ref change_path) => {
|
||||
if let Some(mut route) = path_to_route(&root_path, change_path) {
|
||||
route.insert(0, partition_name.clone());
|
||||
|
||||
vfs.add_change(current_time, route);
|
||||
} else {
|
||||
eprintln!("Failed to get route from {}", change_path.display());
|
||||
}
|
||||
},
|
||||
DebouncedEvent::Rename(ref from_change, ref to_change) => {
|
||||
if let Some(mut route) = path_to_route(&root_path, from_change) {
|
||||
route.insert(0, partition_name.clone());
|
||||
|
||||
vfs.add_change(current_time, route);
|
||||
} else {
|
||||
eprintln!("Failed to get route from {}", from_change.display());
|
||||
}
|
||||
|
||||
if let Some(mut route) = path_to_route(&root_path, to_change) {
|
||||
route.insert(0, partition_name.clone());
|
||||
|
||||
vfs.add_change(current_time, route);
|
||||
} else {
|
||||
eprintln!("Failed to get route from {}", to_change.display());
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(self) {
|
||||
let mut watchers = Vec::new();
|
||||
|
||||
// Create an extra scope so that `vfs` gets dropped and unlocked
|
||||
{
|
||||
let vfs = self.vfs.lock().unwrap();
|
||||
|
||||
for (ref partition_name, ref root_path) in vfs.get_partitions() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(1))
|
||||
.expect("Unable to create watcher! This is a bug in Rojo.");
|
||||
|
||||
match watcher.watch(&root_path, RecursiveMode::Recursive) {
|
||||
Ok(_) => (),
|
||||
Err(_) => {
|
||||
panic!("Unable to watch partition {}, with path {}! Make sure that it's a file or directory.", partition_name, root_path.display());
|
||||
},
|
||||
}
|
||||
|
||||
watchers.push(watcher);
|
||||
|
||||
{
|
||||
let partition_name = partition_name.to_string();
|
||||
let root_path = root_path.to_path_buf();
|
||||
let vfs = self.vfs.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
Self::start_watcher(vfs, rx, partition_name, root_path);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
thread::park();
|
||||
}
|
||||
}
|
||||
}
|
||||
225
server/src/web.rs
Normal file
225
server/src/web.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use std::io::Read;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use rouille;
|
||||
use serde;
|
||||
use serde_json;
|
||||
|
||||
use project::Project;
|
||||
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> {
|
||||
server_version: &'static str,
|
||||
protocol_version: u64,
|
||||
server_id: &'a str,
|
||||
project: &'a Project,
|
||||
current_time: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ReadResult<'a> {
|
||||
items: Vec<Option<RbxInstance>>,
|
||||
server_id: &'a str,
|
||||
current_time: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ChangesResult<'a> {
|
||||
changes: &'a [VfsChange],
|
||||
server_id: &'a str,
|
||||
current_time: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WriteSpecifier {
|
||||
route: String,
|
||||
item: RbxInstance,
|
||||
}
|
||||
|
||||
fn json<T: serde::Serialize>(value: T) -> rouille::Response {
|
||||
let data = serde_json::to_string(&value).unwrap();
|
||||
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;
|
||||
},
|
||||
None => return None,
|
||||
}
|
||||
|
||||
let body = match request.data() {
|
||||
Some(v) => v,
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
let parsed = match String::from_utf8(out) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
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,
|
||||
{
|
||||
let body = match read_json_text(&request) {
|
||||
Some(v) => v,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let parsed = match serde_json::from_str(&body) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
// TODO: Change return type to some sort of Result
|
||||
|
||||
Some(parsed)
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
||||
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();
|
||||
|
||||
vfs.current_time()
|
||||
};
|
||||
|
||||
json(ServerInfo {
|
||||
server_version: env!("CARGO_PKG_VERSION"),
|
||||
protocol_version: 1,
|
||||
server_id: &server_id,
|
||||
project: &project,
|
||||
current_time,
|
||||
})
|
||||
},
|
||||
|
||||
(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);
|
||||
|
||||
json(ChangesResult {
|
||||
changes,
|
||||
server_id: &server_id,
|
||||
current_time,
|
||||
})
|
||||
},
|
||||
|
||||
(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();
|
||||
|
||||
let current_time = vfs.current_time();
|
||||
|
||||
let mut items = Vec::new();
|
||||
|
||||
for route in &read_request {
|
||||
match vfs.read(&route) {
|
||||
Ok(v) => items.push(Some(v)),
|
||||
Err(_) => items.push(None),
|
||||
}
|
||||
}
|
||||
|
||||
(items, current_time)
|
||||
};
|
||||
|
||||
// Transform all of our VfsItem objects into Roblox instances
|
||||
// the client can use.
|
||||
let rbx_items = items
|
||||
.iter()
|
||||
.map(|item| {
|
||||
match *item {
|
||||
Some(ref item) => plugin_chain.transform_file(item),
|
||||
None => None,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if config.verbose {
|
||||
println!("Got read request: {:?}", read_request);
|
||||
println!("Responding with:\n\t{:?}", rbx_items);
|
||||
}
|
||||
|
||||
json(ReadResult {
|
||||
server_id: &server_id,
|
||||
items: rbx_items,
|
||||
current_time,
|
||||
})
|
||||
},
|
||||
|
||||
(POST) (/write) => {
|
||||
// Not yet implemented.
|
||||
|
||||
let _write_request: Vec<WriteSpecifier> = match read_json(&request) {
|
||||
Some(v) => v,
|
||||
None => return rouille::Response::empty_400(),
|
||||
};
|
||||
|
||||
rouille::Response::empty_404()
|
||||
},
|
||||
|
||||
_ => rouille::Response::empty_404()
|
||||
)
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user