Initial commit

This commit is contained in:
Lucien Greathouse
2017-11-29 17:25:37 -08:00
commit 7838b2e67d
30 changed files with 2825 additions and 0 deletions

183
src/bin.rs Normal file
View File

@@ -0,0 +1,183 @@
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate rouille;
#[macro_use]
extern crate clap;
extern crate notify;
extern crate rand;
extern crate serde;
extern crate serde_json;
pub mod web;
pub mod core;
pub mod project;
pub mod pathext;
pub mod vfs;
pub mod vfs_watch;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::thread;
use core::Config;
use pathext::canonicalish;
use project::Project;
use vfs::Vfs;
use vfs_watch::VfsWatcher;
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 dev 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.")
(@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,
};
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);
},
}
},
("serve", sub_matches) => {
let sub_matches = sub_matches.unwrap();
let project_path = match sub_matches.value_of("PROJECT") {
Some(v) => PathBuf::from(v),
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(_) => {
println!("Using default project...");
Project::default()
},
};
let port = {
match sub_matches.value_of("port") {
Some(source) => match source.parse::<u64>() {
Ok(value) => value,
Err(_) => {
eprintln!("Invalid port '{}'", source);
std::process::exit(1);
},
},
None => project.serve_port,
}
};
let config = Config {
port,
verbose,
server_id,
};
if verbose {
println!("Loading VFS...");
}
let vfs = {
let mut vfs = Vfs::new();
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();
});
}
web::start(config.clone(), project.clone(), vfs.clone());
println!("Server listening on port {}", port);
loop {}
},
("pack", _) => {
eprintln!("Not implemented.");
std::process::exit(1);
},
_ => {
eprintln!("Please specify a subcommand!");
eprintln!("Try 'rojo help' for information.");
std::process::exit(1);
},
}
}

6
src/core.rs Normal file
View File

@@ -0,0 +1,6 @@
#[derive(Debug, Clone)]
pub struct Config {
pub port: u64,
pub verbose: bool,
pub server_id: u64,
}

103
src/pathext.rs Normal file
View File

@@ -0,0 +1,103 @@
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);
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"));
}

147
src/project.rs Normal file
View File

@@ -0,0 +1,147 @@
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,
Invalid,
}
#[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 {
pub path: String,
pub target: String,
}
#[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 {
pub fn new<T: Into<String>>(name: T) -> Project {
Project {
name: name.into(),
..Default::default()
}
}
pub fn init<T: AsRef<Path>>(location: T) -> Result<Project, ProjectInitError> {
let location = location.as_ref();
let package_path = location.join(PROJECT_FILENAME);
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),
};
let name = match location.file_name() {
Some(v) => v.to_string_lossy().into_owned(),
None => "new-project".to_string(),
};
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)
}
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(_) => return Err(ProjectLoadError::Invalid),
}
}
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: "some-project".to_string(),
serve_port: 8000,
partitions: HashMap::new(),
}
}
}

181
src/vfs.rs Normal file
View File

@@ -0,0 +1,181 @@
use std::borrow::Borrow;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::Read;
use std::path::{Path, PathBuf};
use std::time::Instant;
/// 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 {
/// Contains all of the partitions mounted by the Vfs.
///
/// These must be absolute paths!
pub partitions: HashMap<String, PathBuf>,
/// When the Vfs was initialized; used for change tracking.
pub start_time: Instant,
/// A chronologically-sorted list of routes that changed since the Vfs was
/// created, along with a timestamp denoting when.
pub change_history: Vec<VfsChange>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VfsChange {
timestamp: f64,
route: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum VfsItem {
File { contents: String },
Dir { children: HashMap<String, VfsItem> },
}
impl Vfs {
pub fn new() -> Vfs {
Vfs {
partitions: HashMap::new(),
start_time: Instant::now(),
change_history: Vec::new(),
}
}
fn route_to_path<R: Borrow<str>>(&self, route: &[R]) -> Option<PathBuf> {
let (partition_name, rest) = match route.split_first() {
Some((first, rest)) => (first.borrow(), rest),
None => return None,
};
let partition = match self.partitions.get(partition_name) {
Some(v) => v,
None => return None,
};
let full_path = {
let joined = rest.join("/");
let relative = Path::new(&joined);
partition.join(relative)
};
Some(full_path)
}
fn read_dir<P: AsRef<Path>>(&self, path: P) -> Result<VfsItem, ()> {
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();
match self.read_path(&path) {
Ok(child_item) => {
let name = path.file_name().unwrap().to_string_lossy().into_owned();
children.insert(name, child_item);
},
Err(_) => {},
}
}
Ok(VfsItem::Dir {
children,
})
}
fn read_file<P: AsRef<Path>>(&self, path: P) -> Result<VfsItem, ()> {
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 {
contents,
})
}
fn read_path<P: AsRef<Path>>(&self, 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(path)
} else if metadata.is_file() {
self.read_file(path)
} else {
Err(())
}
}
pub fn current_time(&self) -> f64 {
let elapsed = self.start_time.elapsed();
elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 * 1e-9
}
pub fn add_change(&mut self, timestamp: f64, route: Vec<String>) {
self.change_history.push(VfsChange {
timestamp,
route,
});
}
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]
}
}
pub fn read<R: Borrow<str>>(&self, route: &[R]) -> Result<VfsItem, ()> {
match self.route_to_path(route) {
Some(path) => self.read_path(&path),
None => Err(()),
}
}
pub fn write<R: Borrow<str>>(&self, _route: &[R], _item: VfsItem) -> Result<(), ()> {
unimplemented!()
}
pub fn delete<R: Borrow<str>>(&self, _route: &[R]) -> Result<(), ()> {
unimplemented!()
}
}

89
src/vfs_watch.rs Normal file
View File

@@ -0,0 +1,89 @@
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
use std::time::Duration;
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
use vfs::Vfs;
use pathext::path_to_route;
pub struct VfsWatcher {
vfs: Arc<Mutex<Vfs>>,
watchers: Vec<RecommendedWatcher>,
}
impl VfsWatcher {
pub fn new(vfs: Arc<Mutex<Vfs>>) -> VfsWatcher {
VfsWatcher {
vfs,
watchers: Vec::new(),
}
}
pub fn start(mut self) {
{
let outer_vfs = self.vfs.lock().unwrap();
for (partition_name, root_path) in &outer_vfs.partitions {
let (tx, rx) = mpsc::channel();
let partition_name = partition_name.clone();
let root_path = root_path.clone();
let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(1))
.expect("Unable to create watcher!");
watcher
.watch(&root_path, RecursiveMode::Recursive)
.expect("Unable to watch path!");
self.watchers.push(watcher);
{
let vfs = self.vfs.clone();
thread::spawn(move || {
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 {
println!("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 {
println!("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 {
println!("Failed to get route from {}", to_change.display());
}
},
_ => {},
}
}
});
}
}
}
loop {}
}
}

170
src/web.rs Normal file
View File

@@ -0,0 +1,170 @@
use std::io::Read;
use std::sync::{Arc, Mutex};
use std::thread;
use rouille;
use serde;
use serde_json;
use core::Config;
use project::Project;
use vfs::{Vfs, VfsItem, VfsChange};
static MAX_BODY_SIZE: usize = 25 * 1024 * 1025; // 25 MiB
#[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<VfsItem>>,
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,
}
fn json<T: serde::Serialize>(value: T) -> rouille::Response {
let data = serde_json::to_string(&value).unwrap();
rouille::Response::from_data("application/json", data)
}
fn read_json_text(request: &rouille::Request) -> Option<String> {
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,
};
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 out.len() > MAX_BODY_SIZE {
return None;
}
let parsed = match String::from_utf8(out) {
Ok(v) => v,
Err(_) => return None,
};
Some(parsed)
}
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,
};
Some(parsed)
}
pub fn start(config: Config, project: Project, vfs: Arc<Mutex<Vfs>>) {
let address = format!("localhost:{}", config.port);
let server_id = config.server_id.to_string();
thread::spawn(move || {
rouille::start_server(address, move |request| {
router!(request,
(GET) (/) => {
let current_time = {
let vfs = vfs.lock().unwrap();
vfs.current_time()
};
json(ServerInfo {
server_version: env!("CARGO_PKG_VERSION"),
protocol_version: 0,
server_id: &server_id,
project: &project,
current_time,
})
},
(GET) (/changes/{ last_time: f64 }) => {
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) => {
let read_request: Vec<Vec<String>> = match read_json(&request) {
Some(v) => v,
None => return rouille::Response::empty_400(),
};
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)
};
json(ReadResult {
server_id: &server_id,
items,
current_time,
})
},
(POST) (/write) => {
rouille::Response::empty_404()
},
_ => rouille::Response::empty_404()
)
});
});
}