diff --git a/server/src/commands/serve.rs b/server/src/commands/serve.rs index f6fed859..456e7c6d 100644 --- a/server/src/commands/serve.rs +++ b/server/src/commands/serve.rs @@ -1,11 +1,16 @@ use std::path::PathBuf; use std::process; use std::time::Instant; +use std::collections::HashMap; +use std::thread; use rand; use project::Project; use web; +use session_config::SessionConfig; +use fs_session::FsSession; +use partition::Partition; pub fn serve(project_path: &PathBuf, port: Option) { let server_id = rand::random::(); @@ -21,14 +26,39 @@ pub fn serve(project_path: &PathBuf, port: Option) { }, }; - let web_config = web::WebConfig { - port: port.unwrap_or(project.serve_port), - server_id, - project: project.clone(), - start_time: Instant::now(), + // let web_config = web::WebConfig { + // port: port.unwrap_or(project.serve_port), + // server_id, + // project: project.clone(), + // start_time: Instant::now(), + // }; + + // println!("Server listening on port {}", web_config.port); + + // web::start(web_config); + + let mut partitions = HashMap::new(); + + for (partition_name, partition) in project.partitions.iter() { + let path = project_path.join(&partition.path); + let target = partition.target.split(".").map(String::from).collect::>(); + + partitions.insert(partition_name.clone(), Partition { + path, + target, + }); + } + + let config = SessionConfig { + partitions }; - println!("Server listening on port {}", web_config.port); + println!("Using session config {:#?}", config); - web::start(web_config); + let mut session = FsSession::new(&config); + session.init(); + + loop { + session.step(); + } } diff --git a/server/src/fs_session.rs b/server/src/fs_session.rs new file mode 100644 index 00000000..5d2fd975 --- /dev/null +++ b/server/src/fs_session.rs @@ -0,0 +1,157 @@ +use std::collections::HashMap; +use std::io::Read; +use std::fs::{self, File}; +use std::sync::mpsc::{channel, Receiver}; +use std::time::Duration; + +use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher, watcher}; + +use file_route::FileRoute; +use session_config::SessionConfig; + +struct FileWatcher { + watcher: RecommendedWatcher, + rx: Receiver, +} + +/// Represents a file or directory that has been read from the filesystem. +#[derive(Debug, Clone)] +pub enum FileItem { + File { + contents: String, + route: FileRoute, + }, + Directory { + children: HashMap, + route: FileRoute, + }, +} + +pub struct FsSession<'a> { + pub config: &'a SessionConfig, + + /// The in-memory files associated with each partition. + pub partition_files: HashMap, + + watchers: HashMap, +} + +impl<'a> FsSession<'a> { + pub fn new(config: &SessionConfig) -> FsSession { + FsSession { + config, + partition_files: HashMap::new(), + watchers: HashMap::new(), + } + } + + pub fn init(&mut self) { + self.load_partitions(); + self.watch_partitions(); + } + + pub fn step(&mut self) { + for (partition_name, watcher) in self.watchers.iter() { + let change_event = match watcher.rx.try_recv() { + Ok(v) => v, + Err(_) => continue, + }; + + println!("Change event on partition {}: {:?}", partition_name, change_event); + } + } + + fn watch_partitions(&mut self) { + for (partition_name, partition) in self.config.partitions.iter() { + let (tx, rx) = channel(); + + let mut watcher = watcher(tx, Duration::from_millis(300)).unwrap(); + + watcher.watch(&partition.path, RecursiveMode::Recursive).unwrap(); + + self.watchers.insert(partition_name.clone(), FileWatcher { + watcher, + rx, + }); + } + } + + fn load_partitions(&mut self) { + for partition_name in self.config.partitions.keys() { + let route = FileRoute { + partition: partition_name.clone(), + route: vec![], + }; + + let file_item = self.read(&route).expect("Couldn't load partitions"); + + self.partition_files.insert(partition_name.clone(), file_item); + } + } + + fn read(&self, route: &FileRoute) -> Result { + let partition_path = &self.config.partitions.get(&route.partition) + .ok_or(())?.path; + let path = route.to_path_buf(partition_path); + + let metadata = fs::metadata(path) + .map_err(|_| ())?; + + if metadata.is_dir() { + self.read_directory(route) + } else if metadata.is_file() { + self.read_file(route) + } else { + Err(()) + } + } + + fn read_file(&self, route: &FileRoute) -> Result { + let partition_path = &self.config.partitions.get(&route.partition) + .ok_or(())?.path; + let path = route.to_path_buf(partition_path); + + let mut file = File::open(path) + .map_err(|_| ())?; + + let mut contents = String::new(); + + file.read_to_string(&mut contents) + .map_err(|_| ())?; + + Ok(FileItem::File { + contents, + route: route.clone(), + }) + } + + fn read_directory(&self, route: &FileRoute) -> Result { + let partition_path = &self.config.partitions.get(&route.partition) + .ok_or(())?.path; + let path = route.to_path_buf(partition_path); + + let reader = fs::read_dir(path) + .map_err(|_| ())?; + + let mut children = HashMap::new(); + + for entry in reader { + let entry = entry + .map_err(|_| ())?; + + let path = entry.path(); + let name = path.file_name().unwrap().to_string_lossy().into_owned(); + + let child_route = route.extended_with(&[&name]); + + let child_item = self.read(&child_route)?; + + children.insert(name, child_item); + } + + Ok(FileItem::Directory { + children, + route: route.clone(), + }) + } +} diff --git a/server/src/lib.rs b/server/src/lib.rs index bf922c12..10d5a995 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -12,10 +12,13 @@ extern crate tempfile; pub mod commands; pub mod file_route; +pub mod fs_session; pub mod id; pub mod partition; pub mod pathext; pub mod project; -pub mod rbx_session; +pub mod rbx; +// pub mod session; +pub mod session_config; pub mod web; pub mod web_util; diff --git a/server/src/partition.rs b/server/src/partition.rs index dec43076..5e9d86e8 100644 --- a/server/src/partition.rs +++ b/server/src/partition.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Partition { /// The path on the filesystem that this partition maps to. pub path: PathBuf, diff --git a/server/src/rbx.rs b/server/src/rbx.rs new file mode 100644 index 00000000..c24ff0a0 --- /dev/null +++ b/server/src/rbx.rs @@ -0,0 +1,21 @@ +use std::collections::HashMap; + +use id::Id; + +// TODO: Switch to enum to represent more value types +pub type RbxValue = String; + +#[derive(Debug, Clone)] +pub struct RbxInstance { + /// Maps to the `Name` property on Instance. + pub name: String, + + /// Maps to the `ClassName` property on Instance. + pub class_name: String, + + /// Maps to the `Parent` property on Instance. + pub parent: Option, + + /// Contains all other properties of an Instance. + pub properties: HashMap, +} diff --git a/server/src/rbx_session.rs b/server/src/rbx_session.rs deleted file mode 100644 index c11d0524..00000000 --- a/server/src/rbx_session.rs +++ /dev/null @@ -1,275 +0,0 @@ -use std::collections::HashMap; -use std::io::Read; -use std::fs::{self, File}; - -use id::{Id, get_id}; -use file_route::FileRoute; -use partition::Partition; - -/// Represents a file or directory that has been read from the filesystem. -#[derive(Debug, Clone)] -enum FileItem { - File { - contents: String, - route: FileRoute, - }, - Directory { - children: HashMap, - route: FileRoute, - }, -} - -// TODO: Switch to enum to represent more value types -type RbxValue = String; - -#[derive(Debug, Clone)] -struct RbxInstance { - /// Maps to the `Name` property on Instance. - pub name: String, - - /// Maps to the `ClassName` property on Instance. - pub class_name: String, - - /// Maps to the `Parent` property on Instance. - pub parent: Option, - - /// Contains all other properties of an Instance. - pub properties: HashMap, -} - -struct RbxSession { - pub partitions: HashMap, - - /// The RbxInstance that represents each partition. - pub partition_instances: HashMap, - - /// The in-memory files associated with each partition. - pub partition_files: HashMap, - - /// The store of all instances in the session. - pub instances: HashMap, - // pub instances_by_route: HashMap, -} - -fn file_to_instance(file_item: &FileItem, partition: &Partition) -> (Id, HashMap) { - match file_item { - &FileItem::File { ref contents, ref route } => { - let mut properties = HashMap::new(); - properties.insert("Value".to_string(), contents.clone()); - - let primary_id = get_id(); - let mut instances = HashMap::new(); - - instances.insert(primary_id, RbxInstance { - name: route.name(partition).to_string(), - class_name: "StringValue".to_string(), - parent: None, - properties, - }); - - (primary_id, instances) - }, - &FileItem::Directory { ref children, ref route } => { - let primary_id = get_id(); - let mut instances = HashMap::new(); - - instances.insert(primary_id, RbxInstance { - name: route.name(partition).to_string(), - class_name: "Folder".to_string(), - parent: None, - properties: HashMap::new(), - }); - - for child_file_item in children.values() { - let (child_id, mut child_instances) = file_to_instance(child_file_item, partition); - - child_instances.get_mut(&child_id).unwrap().parent = Some(primary_id); - - for (instance_id, instance) in child_instances.drain() { - instances.insert(instance_id, instance); - } - } - - (primary_id, instances) - } - } -} - -impl RbxSession { - pub fn new() -> RbxSession { - RbxSession { - partitions: HashMap::new(), - partition_instances: HashMap::new(), - partition_files: HashMap::new(), - instances: HashMap::new(), - } - } - - pub fn init(&mut self) { - self.load_files(); - self.load_instances(); - } - - fn load_files(&mut self) { - for partition_name in self.partitions.keys() { - let route = FileRoute { - partition: partition_name.clone(), - route: vec![], - }; - - let file_item = self.read(&route).expect("Couldn't load partitions"); - - self.partition_files.insert(partition_name.clone(), file_item); - } - } - - fn load_instances(&mut self) { - for (partition_name, file_item) in &self.partition_files { - let partition = self.partitions.get(partition_name).unwrap(); - let (root_id, mut instances) = file_to_instance(&file_item, partition); - - // there has to be an std method for this - // oh well - for (instance_id, instance) in instances.drain() { - self.instances.insert(instance_id, instance); - } - - self.partition_instances.insert(partition_name.clone(), root_id); - } - } - - fn read(&self, route: &FileRoute) -> Result { - let partition_path = &self.partitions.get(&route.partition) - .ok_or(())?.path; - let path = route.to_path_buf(partition_path); - - let metadata = fs::metadata(path) - .map_err(|_| ())?; - - if metadata.is_dir() { - self.read_directory(route) - } else if metadata.is_file() { - self.read_file(route) - } else { - Err(()) - } - } - - fn read_file(&self, route: &FileRoute) -> Result { - let partition_path = &self.partitions.get(&route.partition) - .ok_or(())?.path; - let path = route.to_path_buf(partition_path); - - let mut file = File::open(path) - .map_err(|_| ())?; - - let mut contents = String::new(); - - file.read_to_string(&mut contents) - .map_err(|_| ())?; - - Ok(FileItem::File { - contents, - route: route.clone(), - }) - } - - fn read_directory(&self, route: &FileRoute) -> Result { - let partition_path = &self.partitions.get(&route.partition) - .ok_or(())?.path; - let path = route.to_path_buf(partition_path); - - let reader = fs::read_dir(path) - .map_err(|_| ())?; - - let mut children = HashMap::new(); - - for entry in reader { - let entry = entry - .map_err(|_| ())?; - - let path = entry.path(); - let name = path.file_name().unwrap().to_string_lossy().into_owned(); - - let child_route = route.extended_with(&[&name]); - - let child_item = self.read(&child_route)?; - - children.insert(name, child_item); - } - - Ok(FileItem::Directory { - children, - route: route.clone(), - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile; - - // I'm not exactly sure how I want to structure these tests - // Essentially, I need a bunch of random files to load, and to measure: - // * What FileItems were loaded? - // * Are changes logged to those FileItems correctly? - // * What RbxInstance objects are generated from them? - // * Are changes propagated from FileItem through to those RbxInstances? - - #[test] - fn file_items_correct() { - use std::io::Write; - - let root_dir = tempfile::tempdir().unwrap(); - - let foo_path = root_dir.path().join("foo.txt"); - let bar_path = root_dir.path().join("bar.tsv"); - - { - let mut foo = File::create(foo_path).unwrap(); - writeln!(foo, "Hello, foo!").unwrap(); - - let mut bar = File::create(bar_path).unwrap(); - writeln!(bar, "Hello, bar!").unwrap(); - } - - let mut session = RbxSession::new(); - - let partition = Partition { - path: root_dir.path().to_path_buf(), - target: vec!["ReplicatedStorage".to_string()], - }; - - session.partitions.insert("agh".to_string(), partition); - - session.load_files(); - - assert_eq!(session.partition_files.len(), 1); - - { - let folder = session.partition_files.values().nth(0).unwrap(); - - let children = match folder { - &FileItem::Directory { ref children, .. } => children, - _ => panic!("Not a directory!"), - }; - - assert_eq!(children.len(), 2); - assert!(children.get("foo.txt").is_some()); - assert!(children.get("bar.tsv").is_some()); - } - - session.load_instances(); - - assert_eq!(session.instances.len(), 3); - assert_eq!(session.partition_instances.len(), 1); - - { - let folder_id = session.partition_instances.values().nth(0).unwrap(); - - let folder = session.instances.get(folder_id).unwrap(); - assert_eq!(folder.name, "ReplicatedStorage"); - } - } -} diff --git a/server/src/session.rs b/server/src/session.rs new file mode 100644 index 00000000..f711dbd1 --- /dev/null +++ b/server/src/session.rs @@ -0,0 +1,165 @@ +use std::collections::HashMap; +use std::io::Read; +use std::fs::{self, File}; + +use id::{Id, get_id}; +use file_route::FileRoute; +use partition::Partition; +use rbx::RbxInstance; +use fs_session::FsSession; +use session_config::SessionConfig; + +pub struct Session<'a> { + pub config: &'a SessionConfig, + + /// The RbxInstance that represents each partition. + pub partition_instances: HashMap, + + /// The store of all instances in the session. + pub instances: HashMap, + + // pub instances_by_route: HashMap, +} + +fn file_to_instance(file_item: &FileItem, partition: &Partition) -> (Id, HashMap) { + match file_item { + &FileItem::File { ref contents, ref route } => { + let mut properties = HashMap::new(); + properties.insert("Value".to_string(), contents.clone()); + + let primary_id = get_id(); + let mut instances = HashMap::new(); + + instances.insert(primary_id, RbxInstance { + name: route.name(partition).to_string(), + class_name: "StringValue".to_string(), + parent: None, + properties, + }); + + (primary_id, instances) + }, + &FileItem::Directory { ref children, ref route } => { + let primary_id = get_id(); + let mut instances = HashMap::new(); + + instances.insert(primary_id, RbxInstance { + name: route.name(partition).to_string(), + class_name: "Folder".to_string(), + parent: None, + properties: HashMap::new(), + }); + + for child_file_item in children.values() { + let (child_id, mut child_instances) = file_to_instance(child_file_item, partition); + + child_instances.get_mut(&child_id).unwrap().parent = Some(primary_id); + + for (instance_id, instance) in child_instances.drain() { + instances.insert(instance_id, instance); + } + } + + (primary_id, instances) + } + } +} + +impl Session { + pub fn new() -> Session { + Session { + partitions: HashMap::new(), + partition_instances: HashMap::new(), + instances: HashMap::new(), + } + } + + pub fn init(&mut self) { + self.load_files(); + self.load_instances(); + } + + fn load_instances(&mut self) { + for (partition_name, file_item) in &self.partition_files { + let partition = self.partitions.get(partition_name).unwrap(); + let (root_id, mut instances) = file_to_instance(&file_item, partition); + + // there has to be an std method for this + // oh well + for (instance_id, instance) in instances.drain() { + self.instances.insert(instance_id, instance); + } + + self.partition_instances.insert(partition_name.clone(), root_id); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile; + + // I'm not exactly sure how I want to structure these tests + // Essentially, I need a bunch of random files to load, and to measure: + // * What FileItems were loaded? + // * Are changes logged to those FileItems correctly? + // * What RbxInstance objects are generated from them? + // * Are changes propagated from FileItem through to those RbxInstances? + + // #[test] + // fn file_items_correct() { + // use std::io::Write; + + // let root_dir = tempfile::tempdir().unwrap(); + + // let foo_path = root_dir.path().join("foo.txt"); + // let bar_path = root_dir.path().join("bar.tsv"); + + // { + // let mut foo = File::create(foo_path).unwrap(); + // writeln!(foo, "Hello, foo!").unwrap(); + + // let mut bar = File::create(bar_path).unwrap(); + // writeln!(bar, "Hello, bar!").unwrap(); + // } + + // let mut session = Session::new(); + + // let partition = Partition { + // path: root_dir.path().to_path_buf(), + // target: vec!["ReplicatedStorage".to_string()], + // }; + + // session.partitions.insert("agh".to_string(), partition); + + // session.load_files(); + + // assert_eq!(session.partition_files.len(), 1); + + // { + // let folder = session.partition_files.values().nth(0).unwrap(); + + // let children = match folder { + // &FileItem::Directory { ref children, .. } => children, + // _ => panic!("Not a directory!"), + // }; + + // assert_eq!(children.len(), 2); + // assert!(children.get("foo.txt").is_some()); + // assert!(children.get("bar.tsv").is_some()); + // } + + // session.load_instances(); + + // assert_eq!(session.instances.len(), 3); + // assert_eq!(session.partition_instances.len(), 1); + + // { + // let folder_id = session.partition_instances.values().nth(0).unwrap(); + + // let folder = session.instances.get(folder_id).unwrap(); + // assert_eq!(folder.name, "ReplicatedStorage"); + // } + // } +} diff --git a/server/src/session_config.rs b/server/src/session_config.rs new file mode 100644 index 00000000..8cd68a99 --- /dev/null +++ b/server/src/session_config.rs @@ -0,0 +1,8 @@ +use std::collections::HashMap; + +use partition::Partition; + +#[derive(Debug, Clone, PartialEq)] +pub struct SessionConfig { + pub partitions: HashMap, +}