diff --git a/Cargo.lock b/Cargo.lock
index 9bd31bd2..5d3837cb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -277,6 +277,14 @@ dependencies = [
"cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "crossbeam-channel"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "crossbeam-deque"
version = "0.7.1"
@@ -701,6 +709,11 @@ name = "itoa"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
+[[package]]
+name = "jod-thread"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
[[package]]
name = "kernel32-sys"
version = "0.2.2"
@@ -1501,11 +1514,13 @@ name = "rojo"
version = "0.5.0"
dependencies = [
"clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crossbeam-channel 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.12.33 (registry+https://github.com/rust-lang/crates.io-index)",
+ "jod-thread 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2175,6 +2190,7 @@ dependencies = [
"checksum core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d"
"checksum core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
"checksum crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
+"checksum crossbeam-channel 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c8ec7fcd21571dc78f96cc96243cab8d8f035247c3efd16c687be154c3fa9efa"
"checksum crossbeam-deque 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b18cd2e169ad86297e6bc0ad9aa679aee9daa4f19e8163860faf7c164e4f5a71"
"checksum crossbeam-epoch 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fedcd6772e37f3da2a9af9bf12ebe046c0dfe657992377b4df982a2b54cd37a9"
"checksum crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b"
@@ -2223,6 +2239,7 @@ dependencies = [
"checksum insta 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "00eef45accbe65bfb859ad16649c6b4bed246768d89493473d9ab6c6a0eb908f"
"checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08"
"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f"
+"checksum jod-thread 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f52a11f73b88fab829a0e4d9e13ea5982c7ac457c72eb3541d82a4afdfce4ff"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
"checksum lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f"
diff --git a/design.gv b/design.gv
new file mode 100644
index 00000000..f44e7db7
--- /dev/null
+++ b/design.gv
@@ -0,0 +1,30 @@
+digraph Rojo {
+ concentrate = true;
+ node [fontname = "sans-serif"];
+
+ plugin [label="Roblox Studio Plugin"]
+ session [label="Session"]
+ rbx_tree [label="Instance Tree"]
+ imfs [label="In-Memory Filesystem"]
+ fs_impl [label="Filesystem Implementation\n(stubbed in tests)"]
+ fs [label="Real Filesystem"]
+ snapshot_subsystem [label="Snapshot Subsystem\n(reconciler)"]
+ snapshot_generator [label="Snapshot Generator"]
+ user_middleware [label="User Middleware\n(MoonScript, etc.)"]
+ builtin_middleware [label="Built-in Middleware\n(.lua, .rbxm, etc.)"]
+ api [label="Web API"]
+ file_watcher [label="File Watcher"]
+
+ session -> imfs
+ session -> rbx_tree
+ session -> snapshot_subsystem
+ session -> snapshot_generator
+ session -> file_watcher [dir="both"]
+ file_watcher -> imfs
+ snapshot_generator -> user_middleware
+ snapshot_generator -> builtin_middleware
+ plugin -> api [style="dotted"; dir="both"; minlen=2]
+ api -> session
+ imfs -> fs_impl
+ fs_impl -> fs
+}
\ No newline at end of file
diff --git a/rojo-test/build-tests/plain.txt b/rojo-test/build-tests/plain.txt
new file mode 100644
index 00000000..8948f215
--- /dev/null
+++ b/rojo-test/build-tests/plain.txt
@@ -0,0 +1 @@
+This is a bare text file with no project.
\ No newline at end of file
diff --git a/rojo-test/build-tests/plain_gitkeep/.gitkeep b/rojo-test/build-tests/plain_gitkeep/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/rojo-test/build-tests/rbxmx_ref.rbxmx b/rojo-test/build-tests/rbxmx_ref.rbxmx
new file mode 100644
index 00000000..fcfc2af3
--- /dev/null
+++ b/rojo-test/build-tests/rbxmx_ref.rbxmx
@@ -0,0 +1,25 @@
+
+ true
+ null
+ nil
+ -
+
+ Container
+
+
+
-
+
+ Target
+
+ Pointed to by ObjectValue
+
+
+ -
+
+ Pointer
+
+
[RBX42D96C32E905427DBA530A1881C962FD]
+
+
+
+
\ No newline at end of file
diff --git a/rojo-test/build-tests/txt/default.project.json b/rojo-test/build-tests/txt/default.project.json
new file mode 100644
index 00000000..4590455f
--- /dev/null
+++ b/rojo-test/build-tests/txt/default.project.json
@@ -0,0 +1,6 @@
+{
+ "name": "txt",
+ "tree": {
+ "$path": "foo.txt"
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/build-tests/txt/foo.txt b/rojo-test/build-tests/txt/foo.txt
new file mode 100644
index 00000000..8bba57d2
--- /dev/null
+++ b/rojo-test/build-tests/txt/foo.txt
@@ -0,0 +1 @@
+This is a txt file in a project.
\ No newline at end of file
diff --git a/rojo-test/src/build_test.rs b/rojo-test/src/build_test.rs
index fa15b73e..f45e520a 100644
--- a/rojo-test/src/build_test.rs
+++ b/rojo-test/src/build_test.rs
@@ -31,13 +31,25 @@ gen_build_tests! {
json_model_legacy_name,
module_in_folder,
module_init,
+ plain_gitkeep,
rbxm_in_folder,
rbxmx_in_folder,
server_in_folder,
server_init,
+ txt,
txt_in_folder,
}
+#[test]
+fn build_plain_txt() {
+ run_build_test("plain.txt");
+}
+
+#[test]
+fn build_rbxmx_ref() {
+ run_build_test("rbxmx_ref.rbxmx");
+}
+
fn run_build_test(test_name: &str) {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let build_test_path = manifest_dir.join("build-tests");
diff --git a/rojo-test/src/snapshots/build_test__plain.txt.snap b/rojo-test/src/snapshots/build_test__plain.txt.snap
new file mode 100644
index 00000000..93316de9
--- /dev/null
+++ b/rojo-test/src/snapshots/build_test__plain.txt.snap
@@ -0,0 +1,14 @@
+---
+created: "2019-08-09T00:17:42.253380600Z"
+creator: insta@0.10.0
+source: rojo-test/src/build_test.rs
+expression: contents
+---
+
+ -
+
+ plain
+ This is a bare text file with no project.
+
+
+
diff --git a/rojo-test/src/snapshots/build_test__plain_gitkeep.snap b/rojo-test/src/snapshots/build_test__plain_gitkeep.snap
new file mode 100644
index 00000000..204b47e9
--- /dev/null
+++ b/rojo-test/src/snapshots/build_test__plain_gitkeep.snap
@@ -0,0 +1,13 @@
+---
+created: "2019-08-09T00:17:42.175575800Z"
+creator: insta@0.10.0
+source: rojo-test/src/build_test.rs
+expression: contents
+---
+
+ -
+
+ plain_gitkeep
+
+
+
diff --git a/rojo-test/src/snapshots/build_test__rbxmx_ref.rbxmx.snap b/rojo-test/src/snapshots/build_test__rbxmx_ref.rbxmx.snap
new file mode 100644
index 00000000..c631c482
--- /dev/null
+++ b/rojo-test/src/snapshots/build_test__rbxmx_ref.rbxmx.snap
@@ -0,0 +1,28 @@
+---
+created: "2019-08-10T07:57:42.835269100Z"
+creator: insta@0.10.0
+source: rojo-test/src/build_test.rs
+expression: contents
+---
+
+ -
+
+ rbxmx_ref
+
+
+
-
+
+ Target
+
+ Pointed to by ObjectValue
+
+
+ -
+
+ Pointer
+
+
[1]
+
+
+
+
\ No newline at end of file
diff --git a/rojo-test/src/snapshots/build_test__txt.snap b/rojo-test/src/snapshots/build_test__txt.snap
new file mode 100644
index 00000000..4794a928
--- /dev/null
+++ b/rojo-test/src/snapshots/build_test__txt.snap
@@ -0,0 +1,14 @@
+---
+created: "2019-08-09T00:22:01.983322Z"
+creator: insta@0.10.0
+source: rojo-test/src/build_test.rs
+expression: contents
+---
+
+ -
+
+ txt
+ This is a txt file in a project.
+
+
+
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 10735387..6f15bcf3 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -21,29 +21,31 @@ path = "src/bin.rs"
[dependencies]
clap = "2.27"
+crossbeam-channel = "0.3.9"
csv = "1.0"
env_logger = "0.6"
failure = "0.1.3"
futures = "0.1"
hyper = "0.12"
+jod-thread = "0.1.0"
log = "0.4"
maplit = "1.0.1"
notify = "4.0"
rbx_binary = "0.4.1"
rbx_dom_weak = "1.9.0"
-rbx_xml = "0.11.0"
rbx_reflection = "3.1.388"
+rbx_xml = "0.11.0"
regex = "1.0"
reqwest = "0.9.5"
-rlua = "0.16"
ritz = "0.1.0"
+rlua = "0.16"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "0.7", features = ["v4", "serde"] }
[dev-dependencies]
-tempfile = "3.0"
-walkdir = "2.1"
lazy_static = "1.2"
+paste = "0.1"
pretty_assertions = "0.6.1"
-paste = "0.1"
\ No newline at end of file
+tempfile = "3.0"
+walkdir = "2.1"
\ No newline at end of file
diff --git a/server/assets/test-folder.rbxm b/server/assets/test-folder.rbxm
new file mode 100644
index 00000000..98da0412
Binary files /dev/null and b/server/assets/test-folder.rbxm differ
diff --git a/server/src/commands/build.rs b/server/src/commands/build.rs
index fbbb2e2f..e8efc6c3 100644
--- a/server/src/commands/build.rs
+++ b/server/src/commands/build.rs
@@ -1,17 +1,17 @@
use std::{
- path::PathBuf,
+ collections::HashMap,
fs::File,
io::{self, Write, BufWriter},
+ path::PathBuf,
};
-use log::info;
+use rbx_dom_weak::{RbxTree, RbxInstanceProperties};
use failure::Fail;
use crate::{
- imfs::{Imfs, FsError},
- project::{Project, ProjectLoadError},
- rbx_session::construct_oneoff_tree,
- rbx_snapshot::SnapshotError,
+ imfs::new::{Imfs, RealFetcher, WatchMode, FsError},
+ snapshot::{apply_patch_set, compute_patch_set},
+ snapshot_middleware::snapshot_from_imfs,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -46,9 +46,6 @@ pub enum BuildError {
#[fail(display = "Could not detect what kind of file to create")]
UnknownOutputKind,
- #[fail(display = "Project load error: {}", _0)]
- ProjectLoadError(#[fail(cause)] ProjectLoadError),
-
#[fail(display = "IO error: {}", _0)]
IoError(#[fail(cause)] io::Error),
@@ -60,18 +57,13 @@ pub enum BuildError {
#[fail(display = "{}", _0)]
FsError(#[fail(cause)] FsError),
-
- #[fail(display = "{}", _0)]
- SnapshotError(#[fail(cause)] SnapshotError),
}
impl_from!(BuildError {
- ProjectLoadError => ProjectLoadError,
io::Error => IoError,
rbx_xml::EncodeError => XmlModelEncodeError,
rbx_binary::EncodeError => BinaryModelEncodeError,
FsError => FsError,
- SnapshotError => SnapshotError,
});
fn xml_encode_config() -> rbx_xml::EncodeOptions {
@@ -84,18 +76,34 @@ pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
.or_else(|| detect_output_kind(options))
.ok_or(BuildError::UnknownOutputKind)?;
- info!("Hoping to generate file of type {:?}", output_kind);
+ log::info!("Hoping to generate file of type {:?}", output_kind);
- info!("Looking for project at {}", options.fuzzy_project_path.display());
+ let mut tree = RbxTree::new(RbxInstanceProperties {
+ name: "ROOT".to_owned(),
+ class_name: "Folder".to_owned(),
+ properties: HashMap::new(),
+ });
+ let root_id = tree.get_root_id();
- let project = Project::load_fuzzy(&options.fuzzy_project_path)?;
+ log::trace!("Constructing in-memory filesystem");
+ let mut imfs = Imfs::new(RealFetcher::new(WatchMode::Disabled));
- info!("Found project at {}", project.file_location.display());
- info!("Using project {:#?}", project);
+ log::trace!("Reading project root");
+ let entry = imfs.get(&options.fuzzy_project_path)
+ .expect("could not get project path");
- let mut imfs = Imfs::new();
- imfs.add_roots_from_project(&project)?;
- let tree = construct_oneoff_tree(&project, &imfs)?;
+ log::trace!("Generating snapshot of instances from IMFS");
+ let snapshot = snapshot_from_imfs(&mut imfs, &entry)
+ .expect("snapshot failed")
+ .expect("snapshot did not return an instance");
+
+ log::trace!("Computing patch set");
+ let patch_set = compute_patch_set(&snapshot, &tree, root_id);
+
+ log::trace!("Applying patch set");
+ apply_patch_set(&mut tree, &patch_set);
+
+ log::trace!("Opening output file for write");
let mut file = BufWriter::new(File::create(&options.output_file)?);
match output_kind {
@@ -103,19 +111,16 @@ pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
// Model files include the root instance of the tree and all its
// descendants.
- let root_id = tree.get_root_id();
rbx_xml::to_writer(&mut file, &tree, &[root_id], xml_encode_config())?;
},
OutputKind::Rbxlx => {
// Place files don't contain an entry for the DataModel, but our
// RbxTree representation does.
- let root_id = tree.get_root_id();
let top_level_ids = tree.get_instance(root_id).unwrap().get_children_ids();
rbx_xml::to_writer(&mut file, &tree, top_level_ids, xml_encode_config())?;
},
OutputKind::Rbxm => {
- let root_id = tree.get_root_id();
rbx_binary::encode(&tree, &[root_id], &mut file)?;
},
OutputKind::Rbxl => {
@@ -123,7 +128,6 @@ pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
log::warn!("Using the XML place format (rbxlx) is recommended instead.");
log::warn!("For more info, see https://github.com/LPGhatguy/rojo/issues/180");
- let root_id = tree.get_root_id();
let top_level_ids = tree.get_instance(root_id).unwrap().get_children_ids();
rbx_binary::encode(&tree, top_level_ids, &mut file)?;
},
@@ -131,5 +135,7 @@ pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
file.flush()?;
+ log::trace!("Done!");
+
Ok(())
}
\ No newline at end of file
diff --git a/server/src/commands/serve.rs b/server/src/commands/serve.rs
index 567c35ef..fbaaaed4 100644
--- a/server/src/commands/serve.rs
+++ b/server/src/commands/serve.rs
@@ -1,16 +1,19 @@
use std::{
+ collections::HashMap,
path::PathBuf,
sync::Arc,
};
-use log::info;
+use rbx_dom_weak::{RbxTree, RbxInstanceProperties};
use failure::Fail;
use crate::{
+ imfs::new::{Imfs, RealFetcher, WatchMode},
project::{Project, ProjectLoadError},
+ serve_session::ServeSession,
+ snapshot::{apply_patch_set, compute_patch_set},
+ snapshot_middleware::snapshot_from_imfs,
web::LiveServer,
- imfs::FsError,
- live_session::{LiveSession, LiveSessionError},
};
const DEFAULT_PORT: u16 = 34872;
@@ -23,40 +26,64 @@ pub struct ServeOptions {
#[derive(Debug, Fail)]
pub enum ServeError {
- #[fail(display = "Project load error: {}", _0)]
- ProjectLoadError(#[fail(cause)] ProjectLoadError),
-
- #[fail(display = "{}", _0)]
- FsError(#[fail(cause)] FsError),
-
- #[fail(display = "{}", _0)]
- LiveSessionError(#[fail(cause)] LiveSessionError),
+ #[fail(display = "Couldn't load project: {}", _0)]
+ ProjectLoad(#[fail(cause)] ProjectLoadError),
}
impl_from!(ServeError {
- ProjectLoadError => ProjectLoadError,
- FsError => FsError,
- LiveSessionError => LiveSessionError,
+ ProjectLoadError => ProjectLoad,
});
pub fn serve(options: &ServeOptions) -> Result<(), ServeError> {
- info!("Looking for project at {}", options.fuzzy_project_path.display());
-
- let project = Arc::new(Project::load_fuzzy(&options.fuzzy_project_path)?);
-
- info!("Found project at {}", project.file_location.display());
- info!("Using project {:#?}", project);
-
- let live_session = Arc::new(LiveSession::new(Arc::clone(&project))?);
- let server = LiveServer::new(live_session);
+ let maybe_project = match Project::load_fuzzy(&options.fuzzy_project_path) {
+ Ok(project) => Some(project),
+ Err(ProjectLoadError::NotFound) => None,
+ Err(other) => return Err(other.into()),
+ };
let port = options.port
- .or(project.serve_port)
+ .or(maybe_project.as_ref().and_then(|project| project.serve_port))
.unwrap_or(DEFAULT_PORT);
println!("Rojo server listening on port {}", port);
+ let mut tree = RbxTree::new(RbxInstanceProperties {
+ name: "ROOT".to_owned(),
+ class_name: "Folder".to_owned(),
+ properties: HashMap::new(),
+ });
+ let root_id = tree.get_root_id();
+
+ let mut imfs = Imfs::new(RealFetcher::new(WatchMode::Enabled));
+ let entry = imfs.get(&options.fuzzy_project_path)
+ .expect("could not get project path");
+
+ let snapshot = snapshot_from_imfs(&mut imfs, &entry)
+ .expect("snapshot failed")
+ .expect("snapshot did not return an instance");
+
+ let patch_set = compute_patch_set(&snapshot, &tree, root_id);
+ apply_patch_set(&mut tree, &patch_set);
+
+ let session = Arc::new(ServeSession::new(maybe_project));
+ let server = LiveServer::new(session);
+
server.start(port);
+ // let receiver = imfs.change_receiver();
+
+ // while let Ok(change) = receiver.recv() {
+ // imfs.commit_change(&change)
+ // .expect("Failed to commit Imfs change");
+
+ // use notify::DebouncedEvent;
+ // if let DebouncedEvent::Write(path) = change {
+ // let contents = imfs.get_contents(path)
+ // .expect("Failed to read changed path");
+
+ // println!("{:?}", std::str::from_utf8(contents));
+ // }
+ // }
+
Ok(())
}
\ No newline at end of file
diff --git a/server/src/commands/upload.rs b/server/src/commands/upload.rs
index c8c0650c..a6218a19 100644
--- a/server/src/commands/upload.rs
+++ b/server/src/commands/upload.rs
@@ -1,56 +1,13 @@
-use std::{
- path::PathBuf,
- io,
-};
+use std::path::PathBuf;
-use log::info;
use failure::Fail;
-use reqwest::header::{ACCEPT, USER_AGENT, CONTENT_TYPE, COOKIE};
-
-use crate::{
- imfs::{Imfs, FsError},
- project::{Project, ProjectLoadError},
- rbx_session::construct_oneoff_tree,
- rbx_snapshot::SnapshotError,
-};
-
#[derive(Debug, Fail)]
pub enum UploadError {
- #[fail(display = "Roblox API Error: {}", _0)]
- RobloxApiError(String),
-
- #[fail(display = "Invalid asset kind: {}", _0)]
- InvalidKind(String),
-
- #[fail(display = "Project load error: {}", _0)]
- ProjectLoadError(#[fail(cause)] ProjectLoadError),
-
- #[fail(display = "IO error: {}", _0)]
- IoError(#[fail(cause)] io::Error),
-
- #[fail(display = "HTTP error: {}", _0)]
- HttpError(#[fail(cause)] reqwest::Error),
-
- #[fail(display = "XML model file error")]
- XmlModelEncodeError(rbx_xml::EncodeError),
-
- #[fail(display = "{}", _0)]
- FsError(#[fail(cause)] FsError),
-
- #[fail(display = "{}", _0)]
- SnapshotError(#[fail(cause)] SnapshotError),
+ #[fail(display = "This error cannot happen")]
+ StubError,
}
-impl_from!(UploadError {
- ProjectLoadError => ProjectLoadError,
- io::Error => IoError,
- reqwest::Error => HttpError,
- rbx_xml::EncodeError => XmlModelEncodeError,
- FsError => FsError,
- SnapshotError => SnapshotError,
-});
-
#[derive(Debug)]
pub struct UploadOptions<'a> {
pub fuzzy_project_path: PathBuf,
@@ -59,49 +16,6 @@ pub struct UploadOptions<'a> {
pub kind: Option<&'a str>,
}
-pub fn upload(options: &UploadOptions) -> Result<(), UploadError> {
- // TODO: Switch to uploading binary format?
-
- info!("Looking for project at {}", options.fuzzy_project_path.display());
-
- let project = Project::load_fuzzy(&options.fuzzy_project_path)?;
-
- info!("Found project at {}", project.file_location.display());
- info!("Using project {:#?}", project);
-
- let mut imfs = Imfs::new();
- imfs.add_roots_from_project(&project)?;
- let tree = construct_oneoff_tree(&project, &imfs)?;
-
- let root_id = tree.get_root_id();
- let mut contents = Vec::new();
-
- match options.kind {
- Some("place") | None => {
- let top_level_ids = tree.get_instance(root_id).unwrap().get_children_ids();
- rbx_xml::to_writer_default(&mut contents, &tree, top_level_ids)?;
- },
- Some("model") => {
- rbx_xml::to_writer_default(&mut contents, &tree, &[root_id])?;
- },
- Some(invalid) => return Err(UploadError::InvalidKind(invalid.to_owned())),
- }
-
- let url = format!("https://data.roblox.com/Data/Upload.ashx?assetid={}", options.asset_id);
-
- let client = reqwest::Client::new();
- let mut response = client.post(&url)
- .header(COOKIE, format!(".ROBLOSECURITY={}", &options.security_cookie))
- .header(USER_AGENT, "Roblox/WinInet")
- .header("Requester", "Client")
- .header(CONTENT_TYPE, "application/xml")
- .header(ACCEPT, "application/json")
- .body(contents)
- .send()?;
-
- if !response.status().is_success() {
- return Err(UploadError::RobloxApiError(response.text()?));
- }
-
- Ok(())
+pub fn upload(_options: &UploadOptions) -> Result<(), UploadError> {
+ unimplemented!("TODO: Reimplement upload command");
}
\ No newline at end of file
diff --git a/server/src/fs_watcher.rs b/server/src/fs_watcher.rs
deleted file mode 100644
index 9641b293..00000000
--- a/server/src/fs_watcher.rs
+++ /dev/null
@@ -1,143 +0,0 @@
-use std::{
- sync::{mpsc, Arc, Mutex},
- time::Duration,
- path::Path,
- ops::Deref,
- thread,
-};
-
-use log::{warn, trace};
-use notify::{
- self,
- DebouncedEvent,
- RecommendedWatcher,
- RecursiveMode,
- Watcher,
-};
-
-use crate::{
- imfs::Imfs,
- rbx_session::RbxSession,
-};
-
-const WATCH_TIMEOUT: Duration = Duration::from_millis(100);
-
-/// Watches for changes on the filesystem and links together the in-memory
-/// filesystem and in-memory Roblox tree.
-pub struct FsWatcher {
- watcher: RecommendedWatcher,
-}
-
-impl FsWatcher {
- /// Start a new FS watcher, watching all of the roots currently attached to
- /// the given Imfs.
- ///
- /// `rbx_session` is optional to make testing easier. If it isn't `None`,
- /// events will be passed to it after they're given to the Imfs.
- pub fn start(imfs: Arc>, rbx_session: Option>>) -> FsWatcher {
- let (watch_tx, watch_rx) = mpsc::channel();
-
- let mut watcher = notify::watcher(watch_tx, WATCH_TIMEOUT)
- .expect("Could not create filesystem watcher");
-
- {
- let imfs = imfs.lock().unwrap();
-
- for root_path in imfs.get_roots() {
- trace!("Watching path {}", root_path.display());
- watcher.watch(root_path, RecursiveMode::Recursive)
- .expect("Could not watch directory");
- }
- }
-
- {
- let imfs = Arc::clone(&imfs);
- let rbx_session = rbx_session.as_ref().map(Arc::clone);
-
- thread::spawn(move || {
- trace!("Watcher thread started");
- while let Ok(event) = watch_rx.recv() {
- // handle_fs_event expects an Option<&Mutex>, but we have
- // an Option>>, so we coerce with Deref.
- let session_ref = rbx_session.as_ref().map(Deref::deref);
-
- handle_fs_event(&imfs, session_ref, event);
- }
- trace!("Watcher thread stopped");
- });
- }
-
- FsWatcher {
- watcher,
- }
- }
-
- pub fn stop_watching_path(&mut self, path: &Path) {
- match self.watcher.unwatch(path) {
- Ok(_) => {},
- Err(e) => {
- warn!("Could not unwatch path {}: {}", path.display(), e);
- },
- }
- }
-}
-
-fn handle_fs_event(imfs: &Mutex, rbx_session: Option<&Mutex>, event: DebouncedEvent) {
- match event {
- DebouncedEvent::Create(path) => {
- trace!("Path created: {}", path.display());
-
- {
- let mut imfs = imfs.lock().unwrap();
- imfs.path_created(&path).unwrap();
- }
-
- if let Some(rbx_session) = rbx_session {
- let mut rbx_session = rbx_session.lock().unwrap();
- rbx_session.path_created(&path);
- }
- },
- DebouncedEvent::Write(path) => {
- trace!("Path created: {}", path.display());
-
- {
- let mut imfs = imfs.lock().unwrap();
- imfs.path_updated(&path).unwrap();
- }
-
- if let Some(rbx_session) = rbx_session {
- let mut rbx_session = rbx_session.lock().unwrap();
- rbx_session.path_updated(&path);
- }
- },
- DebouncedEvent::Remove(path) => {
- trace!("Path removed: {}", path.display());
-
- {
- let mut imfs = imfs.lock().unwrap();
- imfs.path_removed(&path).unwrap();
- }
-
- if let Some(rbx_session) = rbx_session {
- let mut rbx_session = rbx_session.lock().unwrap();
- rbx_session.path_removed(&path);
- }
- },
- DebouncedEvent::Rename(from_path, to_path) => {
- trace!("Path renamed: {} to {}", from_path.display(), to_path.display());
-
- {
- let mut imfs = imfs.lock().unwrap();
- imfs.path_moved(&from_path, &to_path).unwrap();
- }
-
- if let Some(rbx_session) = rbx_session {
- let mut rbx_session = rbx_session.lock().unwrap();
- rbx_session.path_renamed(&from_path, &to_path);
- }
- },
- other => {
- trace!("Unhandled FS event: {:?}", other);
- },
- }
-}
\ No newline at end of file
diff --git a/server/src/imfs.rs b/server/src/imfs.rs
deleted file mode 100644
index 75cb0314..00000000
--- a/server/src/imfs.rs
+++ /dev/null
@@ -1,331 +0,0 @@
-use std::{
- cmp::Ordering,
- collections::{HashMap, HashSet, BTreeSet},
- fmt,
- fs,
- io,
- path::{self, Path, PathBuf},
-};
-
-use failure::Fail;
-use serde::{Serialize, Deserialize};
-
-use crate::project::{Project, ProjectNode};
-
-/// A wrapper around io::Error that also attaches the path associated with the
-/// error.
-#[derive(Debug, Fail)]
-pub struct FsError {
- #[fail(cause)]
- inner: io::Error,
- path: PathBuf,
-}
-
-impl FsError {
- fn new>(inner: io::Error, path: P) -> FsError {
- FsError {
- inner,
- path: path.into(),
- }
- }
-}
-
-impl fmt::Display for FsError {
- fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
- write!(output, "{}: {}", self.path.display(), self.inner)
- }
-}
-
-fn add_sync_points(imfs: &mut Imfs, node: &ProjectNode) -> Result<(), FsError> {
- if let Some(path) = &node.path {
- imfs.add_root(path)?;
- }
-
- for child in node.children.values() {
- add_sync_points(imfs, child)?;
- }
-
- Ok(())
-}
-
-/// The in-memory filesystem keeps a mirror of all files being watched by Rojo
-/// in order to deduplicate file changes in the case of bidirectional syncing
-/// from Roblox Studio.
-///
-/// It also enables Rojo to quickly generate React-like snapshots to make
-/// reasoning about instances and how they relate to files easier.
-#[derive(Debug, Clone)]
-pub struct Imfs {
- items: HashMap,
- roots: HashSet,
-}
-
-impl Imfs {
- pub fn new() -> Imfs {
- Imfs {
- items: HashMap::new(),
- roots: HashSet::new(),
- }
- }
-
- pub fn add_roots_from_project(&mut self, project: &Project) -> Result<(), FsError> {
- add_sync_points(self, &project.tree)
- }
-
- pub fn get_roots(&self) -> &HashSet {
- &self.roots
- }
-
- pub fn get_items(&self) -> &HashMap {
- &self.items
- }
-
- pub fn get(&self, path: &Path) -> Option<&ImfsItem> {
- debug_assert!(path.is_absolute());
- debug_assert!(self.is_within_roots(path));
-
- self.items.get(path)
- }
-
- pub fn add_root(&mut self, path: &Path) -> Result<(), FsError> {
- debug_assert!(path.is_absolute());
-
- if !self.is_within_roots(path) {
- self.roots.insert(path.to_path_buf());
- self.descend_and_read_from_disk(path)?;
- }
-
- Ok(())
- }
-
- pub fn remove_root(&mut self, path: &Path) {
- debug_assert!(path.is_absolute());
-
- if self.roots.get(path).is_some() {
- self.remove_item(path);
-
- if let Some(parent_path) = path.parent() {
- self.unlink_child(parent_path, path);
- }
- }
- }
-
- pub fn path_created(&mut self, path: &Path) -> Result<(), FsError> {
- debug_assert!(path.is_absolute());
- debug_assert!(self.is_within_roots(path));
-
- self.descend_and_read_from_disk(path)
- }
-
- pub fn path_updated(&mut self, path: &Path) -> Result<(), FsError> {
- debug_assert!(path.is_absolute());
- debug_assert!(self.is_within_roots(path));
-
- self.descend_and_read_from_disk(path)
- }
-
- pub fn path_removed(&mut self, path: &Path) -> Result<(), FsError> {
- debug_assert!(path.is_absolute());
- debug_assert!(self.is_within_roots(path));
-
- self.remove_item(path);
-
- if let Some(parent_path) = path.parent() {
- self.unlink_child(parent_path, path);
- }
-
- Ok(())
- }
-
- pub fn path_moved(&mut self, from_path: &Path, to_path: &Path) -> Result<(), FsError> {
- self.path_removed(from_path)?;
- self.path_created(to_path)?;
- Ok(())
- }
-
- pub fn get_root_for_path<'a>(&'a self, path: &Path) -> Option<&'a Path> {
- for root_path in &self.roots {
- if path.starts_with(root_path) {
- return Some(root_path);
- }
- }
-
- None
- }
-
- fn remove_item(&mut self, path: &Path) {
- if let Some(ImfsItem::Directory(directory)) = self.items.remove(path) {
- for child_path in &directory.children {
- self.remove_item(child_path);
- }
- }
- }
-
- fn unlink_child(&mut self, parent: &Path, child: &Path) {
- let parent_item = self.items.get_mut(parent);
-
- match parent_item {
- Some(ImfsItem::Directory(directory)) => {
- directory.children.remove(child);
- },
- _ => {},
- }
- }
-
- fn link_child(&mut self, parent: &Path, child: &Path) {
- if self.is_within_roots(parent) {
- let parent_item = self.items.get_mut(parent);
-
- match parent_item {
- Some(ImfsItem::Directory(directory)) => {
- directory.children.insert(child.to_path_buf());
- },
- _ => {
- panic!("Tried to link child of path that wasn't a directory!");
- },
- }
- }
- }
-
- fn descend_and_read_from_disk(&mut self, path: &Path) -> Result<(), FsError> {
- let root_path = self.get_root_path(path)
- .expect("Tried to descent and read for path that wasn't within roots!");
-
- // If this path is a root, we should read the entire thing.
- if root_path == path {
- self.read_from_disk(path)?;
- return Ok(());
- }
-
- let relative_path = path.strip_prefix(root_path).unwrap();
- let mut current_path = root_path.to_path_buf();
-
- for component in relative_path.components() {
- match component {
- path::Component::Normal(name) => {
- let next_path = current_path.join(name);
-
- if self.items.contains_key(&next_path) {
- current_path = next_path;
- } else {
- break;
- }
- },
- _ => unreachable!(),
- }
- }
-
- self.read_from_disk(¤t_path)
- }
-
- fn read_from_disk(&mut self, path: &Path) -> Result<(), FsError> {
- let metadata = fs::metadata(path)
- .map_err(|e| FsError::new(e, path))?;
-
- if metadata.is_file() {
- let contents = fs::read(path)
- .map_err(|e| FsError::new(e, path))?;
- let item = ImfsItem::File(ImfsFile {
- path: path.to_path_buf(),
- contents,
- });
-
- self.items.insert(path.to_path_buf(), item);
-
- if let Some(parent_path) = path.parent() {
- self.link_child(parent_path, path);
- }
-
- Ok(())
- } else if metadata.is_dir() {
- let item = ImfsItem::Directory(ImfsDirectory {
- path: path.to_path_buf(),
- children: BTreeSet::new(),
- });
-
- self.items.insert(path.to_path_buf(), item);
-
- let dir_children = fs::read_dir(path)
- .map_err(|e| FsError::new(e, path))?;
-
- for entry in dir_children {
- let entry = entry
- .map_err(|e| FsError::new(e, path))?;
-
- let child_path = entry.path();
-
- self.read_from_disk(&child_path)?;
- }
-
- if let Some(parent_path) = path.parent() {
- self.link_child(parent_path, path);
- }
-
- Ok(())
- } else {
- panic!("Unexpected non-file, non-directory item");
- }
- }
-
- fn get_root_path<'a>(&'a self, path: &Path) -> Option<&'a Path> {
- for root_path in &self.roots {
- if path.starts_with(root_path) {
- return Some(root_path)
- }
- }
-
- None
- }
-
- fn is_within_roots(&self, path: &Path) -> bool {
- for root_path in &self.roots {
- if path.starts_with(root_path) {
- return true;
- }
- }
-
- false
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct ImfsFile {
- pub path: PathBuf,
- pub contents: Vec,
-}
-
-impl PartialOrd for ImfsFile {
- fn partial_cmp(&self, other: &Self) -> Option {
- Some(self.cmp(other))
- }
-}
-
-impl Ord for ImfsFile {
- fn cmp(&self, other: &Self) -> Ordering {
- self.path.cmp(&other.path)
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct ImfsDirectory {
- pub path: PathBuf,
- pub children: BTreeSet,
-}
-
-impl PartialOrd for ImfsDirectory {
- fn partial_cmp(&self, other: &Self) -> Option {
- Some(self.cmp(other))
- }
-}
-
-impl Ord for ImfsDirectory {
- fn cmp(&self, other: &Self) -> Ordering {
- self.path.cmp(&other.path)
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
-pub enum ImfsItem {
- File(ImfsFile),
- Directory(ImfsDirectory),
-}
\ No newline at end of file
diff --git a/server/src/imfs/error.rs b/server/src/imfs/error.rs
new file mode 100644
index 00000000..70799785
--- /dev/null
+++ b/server/src/imfs/error.rs
@@ -0,0 +1,65 @@
+use std::{
+ io,
+ fmt,
+ path::PathBuf,
+};
+
+use failure::Fail;
+
+pub type FsResult = Result;
+pub use io::ErrorKind as FsErrorKind;
+
+pub trait FsResultExt {
+ fn with_not_found(self) -> Result