forked from rojo-rbx/rojo
merge impl-v2: server
This commit is contained in:
@@ -1,5 +0,0 @@
|
|||||||
[*.rs]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
|
||||||
149
server/Cargo.lock
generated
149
server/Cargo.lock
generated
@@ -26,7 +26,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atty"
|
name = "atty"
|
||||||
version = "0.2.9"
|
version = "0.2.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@@ -55,7 +55,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.0.1"
|
version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -102,7 +102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -135,8 +135,8 @@ version = "2.31.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"atty 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
"atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@@ -145,7 +145,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc"
|
name = "crc"
|
||||||
version = "1.7.0"
|
version = "1.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@@ -236,7 +236,7 @@ name = "filetime"
|
|||||||
version = "0.1.15"
|
version = "0.1.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
"redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
@@ -264,7 +264,7 @@ name = "fuchsia-zircon"
|
|||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ name = "gzip-header"
|
|||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"enum_primitive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"enum_primitive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -299,7 +299,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
"unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"unicode-normalization 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -347,7 +347,7 @@ name = "log"
|
|||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -385,8 +385,8 @@ version = "1.8.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"mime 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
"phf 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
"phf_codegen 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -438,7 +438,7 @@ name = "net2"
|
|||||||
version = "0.2.32"
|
version = "0.2.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
@@ -524,33 +524,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf"
|
name = "phf"
|
||||||
version = "0.7.21"
|
version = "0.7.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
"phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_codegen"
|
name = "phf_codegen"
|
||||||
version = "0.7.21"
|
version = "0.7.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
"phf_generator 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
"phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_generator"
|
name = "phf_generator"
|
||||||
version = "0.7.21"
|
version = "0.7.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
"phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_shared"
|
name = "phf_shared"
|
||||||
version = "0.7.21"
|
version = "0.7.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@@ -559,7 +559,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "0.3.6"
|
version = "0.3.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@@ -570,7 +570,7 @@ name = "quote"
|
|||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -608,19 +608,19 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "0.2.10"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
"aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"regex-syntax 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"regex-syntax 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.5.5"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@@ -636,17 +636,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rojo"
|
name = "rojo"
|
||||||
version = "0.4.11"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"notify 4.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"notify 4.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
"regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rouille 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rouille 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde_derive 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde_derive 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"tempfile 3.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -662,9 +663,9 @@ dependencies = [
|
|||||||
"multipart 0.13.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"multipart 0.13.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde_derive 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde_derive 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"term 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
"term 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@@ -693,37 +694,27 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.42"
|
version = "1.0.51"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.42"
|
version = "1.0.51"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde_derive_internals 0.23.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"syn 0.13.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_derive_internals"
|
|
||||||
version = "0.23.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
|
||||||
"syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.16"
|
version = "1.0.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -748,10 +739,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "0.13.1"
|
version = "0.13.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
@@ -765,6 +756,18 @@ dependencies = [
|
|||||||
"remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "term"
|
name = "term"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
@@ -863,7 +866,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-normalization"
|
name = "unicode-normalization"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -985,23 +988,23 @@ dependencies = [
|
|||||||
"checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4"
|
"checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4"
|
||||||
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
|
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
|
||||||
"checksum ascii 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae7d751998c189c1d4468cf0a39bb2eae052a9c58d50ebb3b9591ee3813ad50"
|
"checksum ascii 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae7d751998c189c1d4468cf0a39bb2eae052a9c58d50ebb3b9591ee3813ad50"
|
||||||
"checksum atty 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6609a866dd1a1b2d0ee1362195bf3e4f6438abb2d80120b83b1e1f4fb6476dd0"
|
"checksum atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "2fc4a1aa4c24c0718a250f0681885c1af91419d242f29eb8f2ab28502d80dbd1"
|
||||||
"checksum base64 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5032d51da2741729bfdaeb2664d9b8c6d9fd1e2b90715c660b6def36628499c2"
|
"checksum base64 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5032d51da2741729bfdaeb2664d9b8c6d9fd1e2b90715c660b6def36628499c2"
|
||||||
"checksum bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3"
|
"checksum bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3"
|
||||||
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
|
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
|
||||||
"checksum bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3c30d3802dfb7281680d6285f2ccdaa8c2d8fee41f93805dba5c4cf50dc23cf"
|
"checksum bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d0c54bb8f454c567f21197eefcdbf5679d0bd99f2ddbe52e84c77061952e6789"
|
||||||
"checksum brotli-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cb50f54b2e0c671b7ef1637a76237ebacbb293be179440d5d65ca288e42116bb"
|
"checksum brotli-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cb50f54b2e0c671b7ef1637a76237ebacbb293be179440d5d65ca288e42116bb"
|
||||||
"checksum brotli2 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ea9d0bbab1235017a09226b079ed733bca4bf9ecb6b6102bd01aac79ea082dca"
|
"checksum brotli2 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ea9d0bbab1235017a09226b079ed733bca4bf9ecb6b6102bd01aac79ea082dca"
|
||||||
"checksum buf_redux 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b9279646319ff816b05fb5897883ece50d7d854d12b59992683d4f8a71b0f949"
|
"checksum buf_redux 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b9279646319ff816b05fb5897883ece50d7d854d12b59992683d4f8a71b0f949"
|
||||||
"checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39"
|
"checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39"
|
||||||
"checksum byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "73b5bdfe7ee3ad0b99c9801d58807a9dbc9e09196365b0203853b99889ab3c87"
|
"checksum byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "73b5bdfe7ee3ad0b99c9801d58807a9dbc9e09196365b0203853b99889ab3c87"
|
||||||
"checksum bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c129aff112dcc562970abb69e2508b40850dd24c274761bb50fb8a0067ba6c27"
|
"checksum bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c129aff112dcc562970abb69e2508b40850dd24c274761bb50fb8a0067ba6c27"
|
||||||
"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de"
|
"checksum cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "405216fd8fe65f718daa7102ea808a946b6ce40c742998fbfd3463645552de18"
|
||||||
"checksum chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)" = "9213f7cd7c27e95c2b57c49f0e69b1ea65b27138da84a170133fd21b07659c00"
|
"checksum chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)" = "9213f7cd7c27e95c2b57c49f0e69b1ea65b27138da84a170133fd21b07659c00"
|
||||||
"checksum chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1cce36c92cb605414e9b824f866f5babe0a0368e39ea07393b9b63cf3844c0e6"
|
"checksum chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1cce36c92cb605414e9b824f866f5babe0a0368e39ea07393b9b63cf3844c0e6"
|
||||||
"checksum chunked_transfer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "498d20a7aaf62625b9bf26e637cf7736417cde1d0c99f1d04d1170229a85cf87"
|
"checksum chunked_transfer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "498d20a7aaf62625b9bf26e637cf7736417cde1d0c99f1d04d1170229a85cf87"
|
||||||
"checksum clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0f16b89cbb9ee36d87483dc939fe9f1e13c05898d56d7b230a0d4dff033a536"
|
"checksum clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0f16b89cbb9ee36d87483dc939fe9f1e13c05898d56d7b230a0d4dff033a536"
|
||||||
"checksum crc 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bd5d02c0aac6bd68393ed69e00bbc2457f3e89075c6349db7189618dc4ddc1d7"
|
"checksum crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb"
|
||||||
"checksum deflate 0.7.18 (registry+https://github.com/rust-lang/crates.io-index)" = "32c8120d981901a9970a3a1c97cf8b630e0fa8c3ca31e75b6fd6fd5f9f427b31"
|
"checksum deflate 0.7.18 (registry+https://github.com/rust-lang/crates.io-index)" = "32c8120d981901a9970a3a1c97cf8b630e0fa8c3ca31e75b6fd6fd5f9f427b31"
|
||||||
"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
|
"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
|
||||||
"checksum encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec"
|
"checksum encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec"
|
||||||
@@ -1046,33 +1049,33 @@ dependencies = [
|
|||||||
"checksum num-traits 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dee092fcdf725aee04dd7da1d21debff559237d49ef1cb3e69bcb8ece44c7364"
|
"checksum num-traits 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dee092fcdf725aee04dd7da1d21debff559237d49ef1cb3e69bcb8ece44c7364"
|
||||||
"checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30"
|
"checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30"
|
||||||
"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
|
"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
|
||||||
"checksum phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "cb325642290f28ee14d8c6201159949a872f220c62af6e110a56ea914fbe42fc"
|
"checksum phf 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "7d37a244c75a9748e049225155f56dbcb98fe71b192fd25fd23cb914b5ad62f2"
|
||||||
"checksum phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d62594c0bb54c464f633175d502038177e90309daf2e0158be42ed5f023ce88f"
|
"checksum phf_codegen 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "4e4048fe7dd7a06b8127ecd6d3803149126e9b33c7558879846da3a63f734f2b"
|
||||||
"checksum phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "6b07ffcc532ccc85e3afc45865469bf5d9e4ef5bfcf9622e3cfe80c2d275ec03"
|
"checksum phf_generator 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "05a079dd052e7b674d21cb31cbb6c05efd56a2cd2827db7692e2f1a507ebd998"
|
||||||
"checksum phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "07e24b0ca9643bdecd0632f2b3da6b1b89bbb0030e0b992afc1113b23a7bc2f2"
|
"checksum phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "c2261d544c2bb6aa3b10022b0be371b9c7c64f762ef28c6f5d4f1ef6d97b5930"
|
||||||
"checksum proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "49b6a521dc81b643e9a51e0d1cf05df46d5a2f3c0280ea72bcb68276ba64a118"
|
"checksum proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1b06e2f335f48d24442b35a19df506a835fb3547bc3c06ef27340da9acf5cae7"
|
||||||
"checksum quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8"
|
"checksum quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8"
|
||||||
"checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1"
|
"checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1"
|
||||||
"checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5"
|
"checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5"
|
||||||
"checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd"
|
"checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd"
|
||||||
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
|
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
|
||||||
"checksum regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "aec3f58d903a7d2a9dc2bf0e41a746f4530e0cab6b615494e058f67a3ef947fb"
|
"checksum regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "75ecf88252dce580404a22444fc7d626c01815debba56a7f4f536772a5ff19d3"
|
||||||
"checksum regex-syntax 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "bd90079345f4a4c3409214734ae220fd773c6f2e8a543d07370c6c1c369cfbfb"
|
"checksum regex-syntax 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8f1ac0f60d675cc6cf13a20ec076568254472551051ad5dd050364d70671bf6b"
|
||||||
"checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5"
|
"checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5"
|
||||||
"checksum rouille 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc1f8407af80b0630983b2c1f1860dda1960fdec8d3ee75ba8db14937756d3a0"
|
"checksum rouille 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc1f8407af80b0630983b2c1f1860dda1960fdec8d3ee75ba8db14937756d3a0"
|
||||||
"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
|
"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
|
||||||
"checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f"
|
"checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f"
|
||||||
"checksum same-file 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cfb6eded0b06a0b512c8ddbcf04089138c9b4362c2f696f3c3d76039d68f3637"
|
"checksum same-file 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cfb6eded0b06a0b512c8ddbcf04089138c9b4362c2f696f3c3d76039d68f3637"
|
||||||
"checksum serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)" = "a73973861352c932ed1365ce22b32467ce260ac4c8db11cf750ce56334ff2dcf"
|
"checksum serde 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)" = "21924cc18e5281f232a17c040355fac97732b42cf019c24996a1642bcb169cdb"
|
||||||
"checksum serde_derive 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)" = "b392c5a0cebb98121454531c50e60e2ffe0fbeb1a44da277da2d681d08d7dc0b"
|
"checksum serde_derive 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)" = "9c624a90bec6fe9bc60d275d7af71c72c26b24cd6c6776d8e344dc4044caa3e2"
|
||||||
"checksum serde_derive_internals 0.23.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9d30c4596450fd7bbda79ef15559683f9a79ac0193ea819db90000d7e1cae794"
|
"checksum serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "f3ad6d546e765177cf3dded3c2e424a8040f870083a0e64064746b958ece9cb1"
|
||||||
"checksum serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)" = "8c6c4e049dc657a99e394bd85c22acbf97356feeec6dbf44150f2dcf79fb3118"
|
|
||||||
"checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c"
|
"checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c"
|
||||||
"checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537"
|
"checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537"
|
||||||
"checksum slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d807fd58c4181bbabed77cb3b891ba9748241a552bcc5be698faaebefc54f46e"
|
"checksum slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d807fd58c4181bbabed77cb3b891ba9748241a552bcc5be698faaebefc54f46e"
|
||||||
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
|
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
|
||||||
"checksum syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "91b52877572087400e83d24b9178488541e3d535259e04ff17a63df1e5ceff59"
|
"checksum syn 0.13.7 (registry+https://github.com/rust-lang/crates.io-index)" = "61b8f1b737f929c6516ba46a3133fd6d5215ad8a62f66760f851f7048aebedfb"
|
||||||
"checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
|
"checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
|
||||||
|
"checksum tempfile 3.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8cddbd26c5686ece823b507f304c8f188daef548b4cb753512d929ce478a093c"
|
||||||
"checksum term 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "f2077e54d38055cf1ca0fd7933a2e00cd3ec8f6fed352b2a377f06dcdaaf3281"
|
"checksum term 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "f2077e54d38055cf1ca0fd7933a2e00cd3ec8f6fed352b2a377f06dcdaaf3281"
|
||||||
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
||||||
"checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693"
|
"checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693"
|
||||||
@@ -1084,7 +1087,7 @@ dependencies = [
|
|||||||
"checksum ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd2be2d6639d0f8fe6cdda291ad456e23629558d466e2789d2c3e9892bda285d"
|
"checksum ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd2be2d6639d0f8fe6cdda291ad456e23629558d466e2789d2c3e9892bda285d"
|
||||||
"checksum unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33"
|
"checksum unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33"
|
||||||
"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
|
"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
|
||||||
"checksum unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "51ccda9ef9efa3f7ef5d91e8f9b83bbe6955f9bf86aec89d5cce2c874625920f"
|
"checksum unicode-normalization 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "90d662d111b0dbb08a180f2761026cba648c258023c355954a7c00e00e354636"
|
||||||
"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f"
|
"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f"
|
||||||
"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
|
"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
|
||||||
"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
|
"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
|
||||||
|
|||||||
@@ -1,22 +1,29 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rojo"
|
name = "rojo"
|
||||||
version = "0.4.11"
|
version = "0.5.0"
|
||||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||||
description = "A tool to create robust Roblox projects"
|
description = "A tool to create robust Roblox projects"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/LPGhatguy/rojo"
|
repository = "https://github.com/LPGhatguy/rojo"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "librojo"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "rojo"
|
name = "rojo"
|
||||||
path = "src/bin.rs"
|
path = "src/bin.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.27.1"
|
clap = "2.27.1"
|
||||||
rouille = "2.1.0"
|
rouille = "2.1"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
notify = "4.0.0"
|
notify = "4.0.0"
|
||||||
rand = "0.3"
|
rand = "0.4"
|
||||||
regex = "0.2"
|
regex = "1.0"
|
||||||
lazy_static = "1.0"
|
lazy_static = "1.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
|
|||||||
@@ -1,27 +1,11 @@
|
|||||||
#[macro_use] extern crate serde_derive;
|
|
||||||
#[macro_use] extern crate rouille;
|
|
||||||
#[macro_use] extern crate clap;
|
#[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;
|
extern crate librojo;
|
||||||
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::path::{Path, PathBuf};
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
use pathext::canonicalish;
|
use librojo::pathext::canonicalish;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let matches = clap_app!(rojo =>
|
let matches = clap_app!(rojo =>
|
||||||
@@ -40,26 +24,15 @@ fn main() {
|
|||||||
(@arg port: --port +takes_value "The port to listen on. Defaults to 8000.")
|
(@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();
|
).get_matches();
|
||||||
|
|
||||||
let verbose = match matches.occurrences_of("verbose") {
|
|
||||||
0 => false,
|
|
||||||
_ => true,
|
|
||||||
};
|
|
||||||
|
|
||||||
match matches.subcommand() {
|
match matches.subcommand() {
|
||||||
("init", sub_matches) => {
|
("init", sub_matches) => {
|
||||||
let sub_matches = sub_matches.unwrap();
|
let sub_matches = sub_matches.unwrap();
|
||||||
let project_path = Path::new(sub_matches.value_of("PATH").unwrap_or("."));
|
let project_path = Path::new(sub_matches.value_of("PATH").unwrap_or("."));
|
||||||
let full_path = canonicalish(project_path);
|
let full_path = canonicalish(project_path);
|
||||||
|
|
||||||
commands::init(&full_path);
|
librojo::commands::init(&full_path);
|
||||||
},
|
},
|
||||||
("serve", sub_matches) => {
|
("serve", sub_matches) => {
|
||||||
let sub_matches = sub_matches.unwrap();
|
let sub_matches = sub_matches.unwrap();
|
||||||
@@ -82,11 +55,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
commands::serve(&project_path, verbose, port);
|
librojo::commands::serve(&project_path, port);
|
||||||
},
|
|
||||||
("pack", _) => {
|
|
||||||
eprintln!("'rojo pack' is not yet implemented!");
|
|
||||||
process::exit(1);
|
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
eprintln!("Please specify a subcommand!");
|
eprintln!("Please specify a subcommand!");
|
||||||
|
|||||||
@@ -1,98 +1,37 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
use std::process;
|
use std::process;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::fs;
|
||||||
use std::thread;
|
|
||||||
|
|
||||||
use rand;
|
use rand;
|
||||||
|
|
||||||
use project::{Project, ProjectLoadError};
|
use project::Project;
|
||||||
use plugin::{PluginChain};
|
use web::{self, WebConfig};
|
||||||
use plugins::{DefaultPlugin, JsonModelPlugin, ScriptPlugin};
|
use session::Session;
|
||||||
use vfs::{VfsSession, VfsWatcher};
|
|
||||||
use web;
|
|
||||||
|
|
||||||
pub fn serve(project_path: &PathBuf, verbose: bool, port: Option<u64>) {
|
pub fn serve(project_dir: &PathBuf, override_port: Option<u64>) {
|
||||||
let server_id = rand::random::<u64>();
|
let server_id = rand::random::<u64>();
|
||||||
|
|
||||||
let project = match Project::load(project_path) {
|
let project = match Project::load(project_dir) {
|
||||||
Ok(project) => {
|
Ok(v) => {
|
||||||
println!("Using project \"{}\" from {}", project.name, project_path.display());
|
println!("Using project from {}", fs::canonicalize(project_dir).unwrap().display());
|
||||||
project
|
v
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
match err {
|
eprintln!("{}", err);
|
||||||
ProjectLoadError::InvalidJson(serde_err) => {
|
process::exit(1);
|
||||||
eprintln!("Project contained invalid JSON!");
|
|
||||||
eprintln!("{}", project_path.display());
|
|
||||||
eprintln!("Error: {}", 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 {}", project_path.display());
|
|
||||||
|
|
||||||
process::exit(1);
|
|
||||||
},
|
|
||||||
ProjectLoadError::DidNotExist => {
|
|
||||||
eprintln!("Found no project file! Create one using 'rojo init'");
|
|
||||||
eprintln!("Checked for a project at {}", project_path.display());
|
|
||||||
|
|
||||||
process::exit(1);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if project.partitions.len() == 0 {
|
let port = override_port.unwrap_or(project.serve_port);
|
||||||
println!("");
|
|
||||||
println!("This project has no partitions and will not do anything when served!");
|
|
||||||
println!("This is usually a mistake -- edit rojo.json!");
|
|
||||||
println!("");
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static! {
|
println!("Using project {:#?}", project);
|
||||||
static ref PLUGIN_CHAIN: PluginChain = PluginChain::new(vec![
|
|
||||||
Box::new(ScriptPlugin::new()),
|
|
||||||
Box::new(JsonModelPlugin::new()),
|
|
||||||
Box::new(DefaultPlugin::new()),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let vfs = {
|
let mut session = Session::new(project.clone());
|
||||||
let mut vfs = VfsSession::new(&PLUGIN_CHAIN);
|
session.start();
|
||||||
|
|
||||||
for (name, project_partition) in &project.partitions {
|
let web_config = WebConfig::from_session(server_id, port, &session);
|
||||||
let path = {
|
|
||||||
let given_path = Path::new(&project_partition.path);
|
|
||||||
|
|
||||||
if given_path.is_absolute() {
|
println!("Server listening on port {}", port);
|
||||||
given_path.to_path_buf()
|
|
||||||
} else {
|
|
||||||
project_path.join(given_path)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
vfs.insert_partition(name, path);
|
web::start(web_config);
|
||||||
}
|
|
||||||
|
|
||||||
Arc::new(Mutex::new(vfs))
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
let vfs = vfs.clone();
|
|
||||||
thread::spawn(move || {
|
|
||||||
VfsWatcher::new(vfs).start();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let web_config = web::WebConfig {
|
|
||||||
verbose,
|
|
||||||
port: port.unwrap_or(project.serve_port),
|
|
||||||
server_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("Server listening on port {}", web_config.port);
|
|
||||||
|
|
||||||
web::start(web_config, project.clone(), &PLUGIN_CHAIN, vfs.clone());
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
pub type Route = Vec<String>;
|
|
||||||
89
server/src/file_route.rs
Normal file
89
server/src/file_route.rs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
use std::path::{Path, PathBuf, Component};
|
||||||
|
|
||||||
|
use partition::Partition;
|
||||||
|
|
||||||
|
// TODO: Change backing data structure to use a single allocation with slices
|
||||||
|
// taken out of it for each portion
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct FileRoute {
|
||||||
|
pub partition: String,
|
||||||
|
pub route: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileRoute {
|
||||||
|
pub fn from_path(path: &Path, partition: &Partition) -> Option<FileRoute> {
|
||||||
|
assert!(path.is_absolute());
|
||||||
|
|
||||||
|
let relative_path = path.strip_prefix(&partition.path).ok()?;
|
||||||
|
let mut route = Vec::new();
|
||||||
|
|
||||||
|
for component in relative_path.components() {
|
||||||
|
match component {
|
||||||
|
Component::Normal(piece) => {
|
||||||
|
route.push(piece.to_string_lossy().into_owned());
|
||||||
|
},
|
||||||
|
_ => panic!("Unexpected path component: {:?}", component),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(FileRoute {
|
||||||
|
partition: partition.name.clone(),
|
||||||
|
route,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parent(&self) -> Option<FileRoute> {
|
||||||
|
if self.route.len() == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_route = self.route.clone();
|
||||||
|
new_route.pop();
|
||||||
|
|
||||||
|
Some(FileRoute {
|
||||||
|
partition: self.partition.clone(),
|
||||||
|
route: new_route,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a PathBuf out of the `FileRoute` based on the given partition
|
||||||
|
/// `Path`.
|
||||||
|
pub fn to_path_buf(&self, partition_path: &Path) -> PathBuf {
|
||||||
|
let mut result = partition_path.to_path_buf();
|
||||||
|
|
||||||
|
for route_piece in &self.route {
|
||||||
|
result.push(route_piece);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a version of the FileRoute with the given extra pieces appended
|
||||||
|
/// to the end.
|
||||||
|
pub fn extended_with(&self, pieces: &[&str]) -> FileRoute {
|
||||||
|
let mut result = self.clone();
|
||||||
|
|
||||||
|
for piece in pieces {
|
||||||
|
result.route.push(piece.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function is totally wrong and should be handled by middleware, heh.
|
||||||
|
pub fn name(&self, partition: &Partition) -> String { // I guess??
|
||||||
|
if self.route.len() == 0 {
|
||||||
|
// This FileRoute refers to the partition itself
|
||||||
|
|
||||||
|
if partition.target.len() == 0 {
|
||||||
|
// We're targeting the game!
|
||||||
|
"game".to_string()
|
||||||
|
} else {
|
||||||
|
partition.target.last().unwrap().clone()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This FileRoute refers to an item in a partition
|
||||||
|
self.route.last().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
server/src/id.rs
Normal file
21
server/src/id.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
/// A unique identifier, not guaranteed to be generated in any order.
|
||||||
|
pub type Id = usize;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref LAST_ID: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a new ID, which has no defined ordering.
|
||||||
|
pub fn get_id() -> Id {
|
||||||
|
LAST_ID.fetch_add(1, Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_gives_unique_numbers() {
|
||||||
|
let a = get_id();
|
||||||
|
let b = get_id();
|
||||||
|
|
||||||
|
assert!(a != b);
|
||||||
|
}
|
||||||
26
server/src/lib.rs
Normal file
26
server/src/lib.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#[macro_use] extern crate serde_derive;
|
||||||
|
#[macro_use] extern crate rouille;
|
||||||
|
#[macro_use] extern crate lazy_static;
|
||||||
|
extern crate notify;
|
||||||
|
extern crate rand;
|
||||||
|
extern crate serde;
|
||||||
|
extern crate serde_json;
|
||||||
|
extern crate regex;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
extern crate tempfile;
|
||||||
|
|
||||||
|
pub mod commands;
|
||||||
|
pub mod file_route;
|
||||||
|
pub mod id;
|
||||||
|
pub mod message_session;
|
||||||
|
pub mod partition;
|
||||||
|
pub mod partition_watcher;
|
||||||
|
pub mod pathext;
|
||||||
|
pub mod project;
|
||||||
|
pub mod rbx;
|
||||||
|
pub mod rbx_session;
|
||||||
|
pub mod session;
|
||||||
|
pub mod vfs_session;
|
||||||
|
pub mod web;
|
||||||
|
pub mod web_util;
|
||||||
64
server/src/message_session.rs
Normal file
64
server/src/message_session.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{mpsc, Arc, RwLock, Mutex};
|
||||||
|
|
||||||
|
use id::{Id, get_id};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum Message {
|
||||||
|
InstanceChanged {
|
||||||
|
id: Id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MessageSession {
|
||||||
|
pub messages: Arc<RwLock<Vec<Message>>>,
|
||||||
|
pub message_listeners: Arc<Mutex<HashMap<Id, mpsc::Sender<()>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageSession {
|
||||||
|
pub fn new() -> MessageSession {
|
||||||
|
MessageSession {
|
||||||
|
messages: Arc::new(RwLock::new(Vec::new())),
|
||||||
|
message_listeners: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_messages(&self, new_messages: &[Message]) {
|
||||||
|
let message_listeners = self.message_listeners.lock().unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut messages = self.messages.write().unwrap();
|
||||||
|
messages.extend_from_slice(new_messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
for listener in message_listeners.values() {
|
||||||
|
listener.send(()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe(&self, sender: mpsc::Sender<()>) -> Id {
|
||||||
|
let id = get_id();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut message_listeners = self.message_listeners.lock().unwrap();
|
||||||
|
message_listeners.insert(id, sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unsubscribe(&self, id: Id) {
|
||||||
|
{
|
||||||
|
let mut message_listeners = self.message_listeners.lock().unwrap();
|
||||||
|
message_listeners.remove(&id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_message_cursor(&self) -> i32 {
|
||||||
|
self.messages.read().unwrap().len() as i32 - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
13
server/src/partition.rs
Normal file
13
server/src/partition.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Partition {
|
||||||
|
/// The unique name of this partition, used for debugging.
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
/// The path on the filesystem that this partition maps to.
|
||||||
|
pub path: PathBuf,
|
||||||
|
|
||||||
|
/// The route to the Roblox instance that this partition maps to.
|
||||||
|
pub target: Vec<String>,
|
||||||
|
}
|
||||||
65
server/src/partition_watcher.rs
Normal file
65
server/src/partition_watcher.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
use std::sync::mpsc::{channel, Sender};
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher, watcher};
|
||||||
|
|
||||||
|
use partition::Partition;
|
||||||
|
use vfs_session::FileChange;
|
||||||
|
use file_route::FileRoute;
|
||||||
|
|
||||||
|
pub struct PartitionWatcher {
|
||||||
|
pub watcher: RecommendedWatcher,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartitionWatcher {
|
||||||
|
pub fn start_new(partition: Partition, tx: Sender<FileChange>) -> PartitionWatcher {
|
||||||
|
let (watch_tx, watch_rx) = channel();
|
||||||
|
|
||||||
|
let mut watcher = watcher(watch_tx, Duration::from_millis(100)).unwrap();
|
||||||
|
|
||||||
|
watcher.watch(&partition.path, RecursiveMode::Recursive).unwrap();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
loop {
|
||||||
|
match watch_rx.recv() {
|
||||||
|
Ok(event) => {
|
||||||
|
let file_change = match event {
|
||||||
|
DebouncedEvent::Create(path) => {
|
||||||
|
let route = FileRoute::from_path(&path, &partition).unwrap();
|
||||||
|
FileChange::Created(route)
|
||||||
|
},
|
||||||
|
DebouncedEvent::Write(path) => {
|
||||||
|
let route = FileRoute::from_path(&path, &partition).unwrap();
|
||||||
|
FileChange::Updated(route)
|
||||||
|
},
|
||||||
|
DebouncedEvent::Remove(path) => {
|
||||||
|
let route = FileRoute::from_path(&path, &partition).unwrap();
|
||||||
|
FileChange::Deleted(route)
|
||||||
|
},
|
||||||
|
DebouncedEvent::Rename(from_path, to_path) => {
|
||||||
|
let from_route = FileRoute::from_path(&from_path, &partition).unwrap();
|
||||||
|
let to_route = FileRoute::from_path(&to_path, &partition).unwrap();
|
||||||
|
FileChange::Moved(from_route, to_route)
|
||||||
|
},
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
match tx.send(file_change) {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
PartitionWatcher {
|
||||||
|
watcher,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(self) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
use rbx::RbxInstance;
|
|
||||||
use vfs::VfsItem;
|
|
||||||
use core::Route;
|
|
||||||
|
|
||||||
pub enum TransformFileResult {
|
|
||||||
Value(Option<RbxInstance>),
|
|
||||||
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 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_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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use core::Route;
|
|
||||||
use plugin::{Plugin, PluginChain, TransformFileResult, 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()]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
use regex::Regex;
|
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
use core::Route;
|
|
||||||
use plugin::{Plugin, PluginChain, TransformFileResult, FileChangeResult};
|
|
||||||
use rbx::RbxInstance;
|
|
||||||
use vfs::VfsItem;
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref JSON_MODEL_PATTERN: Regex = Regex::new(r"^(.*?)\.model\.json$").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
static JSON_MODEL_INIT: &'static str = "init.model.json";
|
|
||||||
|
|
||||||
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 { ref children, .. } => {
|
|
||||||
let init_item = match children.get(JSON_MODEL_INIT) {
|
|
||||||
Some(v) => v,
|
|
||||||
None => return TransformFileResult::Pass,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut rbx_item = match self.transform_file(plugins, init_item) {
|
|
||||||
TransformFileResult::Value(Some(item)) => item,
|
|
||||||
TransformFileResult::Value(None) | TransformFileResult::Pass => {
|
|
||||||
eprintln!("Inconsistency detected in JsonModelPlugin!");
|
|
||||||
return TransformFileResult::Pass;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
rbx_item.name.clear();
|
|
||||||
rbx_item.name.push_str(vfs_item.name());
|
|
||||||
rbx_item.route = Some(vfs_item.route().to_vec());
|
|
||||||
|
|
||||||
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 == JSON_MODEL_INIT;
|
|
||||||
|
|
||||||
if is_init {
|
|
||||||
let mut changed = route.clone();
|
|
||||||
changed.pop();
|
|
||||||
|
|
||||||
FileChangeResult::MarkChanged(Some(vec![changed]))
|
|
||||||
} else {
|
|
||||||
FileChangeResult::Pass
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
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::*;
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
use core::Route;
|
|
||||||
use plugin::{Plugin, PluginChain, TransformFileResult, 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());
|
|
||||||
rbx_item.route = Some(vfs_item.route().to_vec());
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,18 +2,39 @@ use std::collections::HashMap;
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use rand::{self, Rng};
|
||||||
|
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
|
use partition::Partition;
|
||||||
|
|
||||||
pub static PROJECT_FILENAME: &'static str = "rojo.json";
|
pub static PROJECT_FILENAME: &'static str = "rojo.json";
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ProjectLoadError {
|
pub enum ProjectLoadError {
|
||||||
DidNotExist,
|
DidNotExist(PathBuf),
|
||||||
FailedToOpen,
|
FailedToOpen(PathBuf),
|
||||||
FailedToRead,
|
FailedToRead(PathBuf),
|
||||||
InvalidJson(serde_json::Error),
|
InvalidJson(PathBuf, serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ProjectLoadError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
&ProjectLoadError::InvalidJson(ref project_path, ref serde_err) => {
|
||||||
|
write!(f, "Found invalid JSON reading project: {}\nError: {}", project_path.display(), serde_err)
|
||||||
|
},
|
||||||
|
&ProjectLoadError::FailedToOpen(ref project_path) |
|
||||||
|
&ProjectLoadError::FailedToRead(ref project_path) => {
|
||||||
|
write!(f, "Found project file, but failed to read it: {}", project_path.display())
|
||||||
|
},
|
||||||
|
&ProjectLoadError::DidNotExist(ref project_path) => {
|
||||||
|
write!(f, "Could not locate a project file at {}.\nUse 'rojo init' to create one.", project_path.display())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -34,16 +55,17 @@ impl fmt::Display for ProjectInitError {
|
|||||||
&ProjectInitError::AlreadyExists => {
|
&ProjectInitError::AlreadyExists => {
|
||||||
write!(f, "A project already exists at that location.")
|
write!(f, "A project already exists at that location.")
|
||||||
},
|
},
|
||||||
&ProjectInitError::FailedToCreate | &ProjectInitError::FailedToWrite => {
|
&ProjectInitError::FailedToCreate |
|
||||||
|
&ProjectInitError::FailedToWrite => {
|
||||||
write!(f, "Failed to write to the given location.")
|
write!(f, "Failed to write to the given location.")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ProjectPartition {
|
pub struct SourceProjectPartition {
|
||||||
/// A slash-separated path to a file or folder, relative to the project's
|
/// A slash-separated path to a file or folder, relative to the project's
|
||||||
/// directory.
|
/// directory.
|
||||||
pub path: String,
|
pub path: String,
|
||||||
@@ -52,43 +74,114 @@ pub struct ProjectPartition {
|
|||||||
pub target: String,
|
pub target: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a project configured by a user for use with Rojo. Holds anything
|
/// Represents a Rojo project in the format that's most convenient for users to
|
||||||
/// that can be configured with `rojo.json`.
|
/// edit. This should generally line up with `Project`, but can diverge when
|
||||||
///
|
/// there's either compatibility shims or when the data structures that Rojo
|
||||||
/// In the future, this object will hold dependency information and other handy
|
/// want are too verbose to write in JSON but easy to convert from something
|
||||||
/// configurables
|
/// else.
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
//
|
||||||
|
/// Holds anything that can be configured with `rojo.json`.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
pub struct Project {
|
pub struct SourceProject {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub serve_port: u64,
|
pub serve_port: u64,
|
||||||
pub partitions: HashMap<String, ProjectPartition>,
|
pub partitions: HashMap<String, SourceProjectPartition>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SourceProject {
|
||||||
|
fn default() -> SourceProject {
|
||||||
|
SourceProject {
|
||||||
|
name: "new-project".to_string(),
|
||||||
|
serve_port: 8000,
|
||||||
|
partitions: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a Rojo project in the format that's convenient for Rojo to work
|
||||||
|
/// with.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Project {
|
||||||
|
/// The path to the project file that this project is associated with.
|
||||||
|
pub project_path: PathBuf,
|
||||||
|
|
||||||
|
/// The name of this project, used for user-facing labels.
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
/// The port that this project will run a web server on.
|
||||||
|
pub serve_port: u64,
|
||||||
|
|
||||||
|
/// All of the project's partitions, laid out in an expanded way.
|
||||||
|
pub partitions: HashMap<String, Partition>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Project {
|
impl Project {
|
||||||
/// Creates a new empty Project object with the given name.
|
fn from_source_project(source_project: SourceProject, project_path: PathBuf) -> Project {
|
||||||
pub fn new<T: Into<String>>(name: T) -> Project {
|
let mut partitions = HashMap::new();
|
||||||
|
|
||||||
|
{
|
||||||
|
let project_directory = project_path.parent().unwrap();
|
||||||
|
|
||||||
|
for (partition_name, partition) in source_project.partitions.into_iter() {
|
||||||
|
let path = project_directory.join(&partition.path);
|
||||||
|
let target = partition.target
|
||||||
|
.split(".")
|
||||||
|
.map(String::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
partitions.insert(partition_name.clone(), Partition {
|
||||||
|
path,
|
||||||
|
target,
|
||||||
|
name: partition_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Project {
|
Project {
|
||||||
name: name.into(),
|
project_path,
|
||||||
..Default::default()
|
name: source_project.name,
|
||||||
|
serve_port: source_project.serve_port,
|
||||||
|
partitions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_source_project(&self) -> SourceProject {
|
||||||
|
let mut partitions = HashMap::new();
|
||||||
|
|
||||||
|
for partition in self.partitions.values() {
|
||||||
|
let path = partition.path.strip_prefix(&self.project_path)
|
||||||
|
.unwrap_or_else(|_| &partition.path)
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let target = partition.target.join(".");
|
||||||
|
|
||||||
|
partitions.insert(partition.name.clone(), SourceProjectPartition {
|
||||||
|
path,
|
||||||
|
target,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SourceProject {
|
||||||
|
partitions,
|
||||||
|
name: self.name.clone(),
|
||||||
|
serve_port: self.serve_port,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes a new project inside the given folder path.
|
/// Initializes a new project inside the given folder path.
|
||||||
pub fn init<T: AsRef<Path>>(location: T) -> Result<Project, ProjectInitError> {
|
pub fn init<T: AsRef<Path>>(location: T) -> Result<Project, ProjectInitError> {
|
||||||
let location = location.as_ref();
|
let location = location.as_ref();
|
||||||
let package_path = location.join(PROJECT_FILENAME);
|
let project_path = location.join(PROJECT_FILENAME);
|
||||||
|
|
||||||
// We abort if the project file already exists.
|
// We abort if the project file already exists.
|
||||||
match fs::metadata(&package_path) {
|
fs::metadata(&project_path)
|
||||||
Ok(_) => return Err(ProjectInitError::AlreadyExists),
|
.map_err(|_| ProjectInitError::AlreadyExists)?;
|
||||||
Err(_) => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut file = match File::create(&package_path) {
|
let mut file = File::create(&project_path)
|
||||||
Ok(f) => f,
|
.map_err(|_| ProjectInitError::FailedToCreate)?;
|
||||||
Err(_) => return Err(ProjectInitError::FailedToCreate),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try to give the project a meaningful name.
|
// Try to give the project a meaningful name.
|
||||||
// If we can't, we'll just fall back to a default.
|
// If we can't, we'll just fall back to a default.
|
||||||
@@ -97,69 +190,57 @@ impl Project {
|
|||||||
None => "new-project".to_string(),
|
None => "new-project".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Generate a random port to run the server on.
|
||||||
|
let serve_port = rand::thread_rng().gen_range(2000, 49151);
|
||||||
|
|
||||||
// Configure the project with all of the values we know so far.
|
// Configure the project with all of the values we know so far.
|
||||||
let project = Project::new(name);
|
let source_project = SourceProject {
|
||||||
let serialized = serde_json::to_string_pretty(&project).unwrap();
|
name,
|
||||||
|
serve_port,
|
||||||
|
partitions: HashMap::new(),
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_string_pretty(&source_project).unwrap();
|
||||||
|
|
||||||
match file.write(serialized.as_bytes()) {
|
file.write(serialized.as_bytes())
|
||||||
Ok(_) => {},
|
.map_err(|_| ProjectInitError::FailedToWrite)?;
|
||||||
Err(_) => return Err(ProjectInitError::FailedToWrite),
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(project)
|
Ok(Project::from_source_project(source_project, project_path))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to load a project from the file named PROJECT_FILENAME from the
|
/// Attempts to load a project from the file named PROJECT_FILENAME from the
|
||||||
/// given folder.
|
/// given folder.
|
||||||
pub fn load<T: AsRef<Path>>(location: T) -> Result<Project, ProjectLoadError> {
|
pub fn load<T: AsRef<Path>>(location: T) -> Result<Project, ProjectLoadError> {
|
||||||
let package_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
|
let project_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
|
||||||
|
|
||||||
match fs::metadata(&package_path) {
|
fs::metadata(&project_path)
|
||||||
Ok(_) => {},
|
.map_err(|_| ProjectLoadError::DidNotExist(project_path.clone()))?;
|
||||||
Err(_) => return Err(ProjectLoadError::DidNotExist),
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut file = match File::open(&package_path) {
|
let mut file = File::open(&project_path)
|
||||||
Ok(f) => f,
|
.map_err(|_| ProjectLoadError::FailedToOpen(project_path.clone()))?;
|
||||||
Err(_) => return Err(ProjectLoadError::FailedToOpen),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut contents = String::new();
|
let mut contents = String::new();
|
||||||
|
|
||||||
match file.read_to_string(&mut contents) {
|
file.read_to_string(&mut contents)
|
||||||
Ok(_) => {},
|
.map_err(|_| ProjectLoadError::FailedToRead(project_path.clone()))?;
|
||||||
Err(_) => return Err(ProjectLoadError::FailedToRead),
|
|
||||||
}
|
|
||||||
|
|
||||||
match serde_json::from_str(&contents) {
|
let source_project = serde_json::from_str(&contents)
|
||||||
Ok(v) => Ok(v),
|
.map_err(|e| ProjectLoadError::InvalidJson(project_path.clone(), e))?;
|
||||||
Err(e) => return Err(ProjectLoadError::InvalidJson(e)),
|
|
||||||
}
|
Ok(Project::from_source_project(source_project, project_path))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Saves the given project file to the given folder with the appropriate name.
|
/// Saves the given project file to the given folder with the appropriate name.
|
||||||
pub fn save<T: AsRef<Path>>(&self, location: T) -> Result<(), ProjectSaveError> {
|
pub fn save<T: AsRef<Path>>(&self, location: T) -> Result<(), ProjectSaveError> {
|
||||||
let package_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
|
let project_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
|
||||||
|
|
||||||
let mut file = match File::create(&package_path) {
|
let mut file = File::create(&project_path)
|
||||||
Ok(f) => f,
|
.map_err(|_| ProjectSaveError::FailedToCreate)?;
|
||||||
Err(_) => return Err(ProjectSaveError::FailedToCreate),
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized = serde_json::to_string_pretty(self).unwrap();
|
let source_project = self.as_source_project();
|
||||||
|
let serialized = serde_json::to_string_pretty(&source_project).unwrap();
|
||||||
|
|
||||||
file.write(serialized.as_bytes()).unwrap();
|
file.write(serialized.as_bytes()).unwrap();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Project {
|
|
||||||
fn default() -> Project {
|
|
||||||
Project {
|
|
||||||
name: "new-project".to_string(),
|
|
||||||
serve_port: 8000,
|
|
||||||
partitions: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,38 +1,116 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// Represents data about a Roblox instance
|
use id::Id;
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
// TODO: Switch to enum to represent more value types
|
||||||
|
pub type RbxValue = String;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct RbxInstance {
|
pub struct RbxInstance {
|
||||||
|
/// Maps to the `Name` property on Instance.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
|
/// Maps to the `ClassName` property on Instance.
|
||||||
pub class_name: String,
|
pub class_name: String,
|
||||||
|
|
||||||
#[serde(default = "Vec::new")]
|
/// Contains all other properties of an Instance.
|
||||||
pub children: Vec<RbxInstance>,
|
|
||||||
|
|
||||||
#[serde(default = "HashMap::new")]
|
|
||||||
pub properties: HashMap<String, RbxValue>,
|
pub properties: HashMap<String, RbxValue>,
|
||||||
|
|
||||||
/// The route that this instance was generated from, if there was one.
|
/// All of the children of this instance. Order is relevant to preserve!
|
||||||
pub route: Option<Vec<String>>,
|
pub children: Vec<Id>,
|
||||||
|
|
||||||
|
pub parent: Option<Id>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Any kind value that can be used by Roblox
|
// This seems like a really bad idea?
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
// Why isn't there a blanket impl for this for all T?
|
||||||
#[serde(rename_all = "PascalCase", tag = "Type")]
|
impl<'a> From<&'a RbxInstance> for Cow<'a, RbxInstance> {
|
||||||
pub enum RbxValue {
|
fn from(instance: &'a RbxInstance) -> Cow<'a, RbxInstance> {
|
||||||
#[serde(rename_all = "PascalCase")]
|
Cow::Borrowed(instance)
|
||||||
String {
|
}
|
||||||
value: String,
|
}
|
||||||
},
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
pub struct RbxTree {
|
||||||
Bool {
|
instances: HashMap<Id, RbxInstance>,
|
||||||
value: bool,
|
}
|
||||||
},
|
|
||||||
#[serde(rename_all = "PascalCase")]
|
impl RbxTree {
|
||||||
Number {
|
pub fn new() -> RbxTree {
|
||||||
value: f64,
|
RbxTree {
|
||||||
},
|
instances: HashMap::new(),
|
||||||
|
}
|
||||||
// TODO: Compound types like Vector3
|
}
|
||||||
|
|
||||||
|
pub fn get_all_instances(&self) -> &HashMap<Id, RbxInstance> {
|
||||||
|
&self.instances
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_instance(&mut self, id: Id, instance: RbxInstance) {
|
||||||
|
if let Some(parent_id) = instance.parent {
|
||||||
|
if let Some(mut parent) = self.instances.get_mut(&parent_id) {
|
||||||
|
if !parent.children.contains(&id) {
|
||||||
|
parent.children.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.instances.insert(id, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_instance(&mut self, id: Id) -> Vec<Id> {
|
||||||
|
let mut ids_to_visit = vec![id];
|
||||||
|
let mut ids_deleted = Vec::new();
|
||||||
|
|
||||||
|
for instance in self.instances.values_mut() {
|
||||||
|
match instance.children.iter().position(|&v| v == id) {
|
||||||
|
Some(index) => {
|
||||||
|
instance.children.remove(index);
|
||||||
|
},
|
||||||
|
None => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let id = match ids_to_visit.pop() {
|
||||||
|
Some(id) => id,
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.instances.get(&id) {
|
||||||
|
Some(instance) => ids_to_visit.extend_from_slice(&instance.children),
|
||||||
|
None => continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.instances.remove(&id);
|
||||||
|
ids_deleted.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
ids_deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_instance<'a, 'b, T>(&'a self, id: Id, output: &'b mut HashMap<Id, T>)
|
||||||
|
where T: From<&'a RbxInstance>
|
||||||
|
{
|
||||||
|
let mut ids_to_visit = vec![id];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let id = match ids_to_visit.pop() {
|
||||||
|
Some(id) => id,
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.instances.get(&id) {
|
||||||
|
Some(instance) => {
|
||||||
|
output.insert(id, instance.into());
|
||||||
|
|
||||||
|
for child_id in &instance.children {
|
||||||
|
ids_to_visit.push(*child_id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
231
server/src/rbx_session.rs
Normal file
231
server/src/rbx_session.rs
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
use file_route::FileRoute;
|
||||||
|
use id::{Id, get_id};
|
||||||
|
use message_session::{Message, MessageSession};
|
||||||
|
use partition::Partition;
|
||||||
|
use project::Project;
|
||||||
|
use rbx::{RbxInstance, RbxTree};
|
||||||
|
use vfs_session::{VfsSession, FileItem, FileChange};
|
||||||
|
|
||||||
|
// TODO: Rethink data structure and insertion/update behavior. Maybe break some
|
||||||
|
// pieces off into a new object?
|
||||||
|
fn file_to_instances(
|
||||||
|
file_item: &FileItem,
|
||||||
|
partition: &Partition,
|
||||||
|
tree: &mut RbxTree,
|
||||||
|
instances_by_route: &mut HashMap<FileRoute, Id>,
|
||||||
|
parent_id: Option<Id>,
|
||||||
|
) -> (Id, Vec<Id>) {
|
||||||
|
match file_item {
|
||||||
|
FileItem::File { contents, route } => {
|
||||||
|
let primary_id = match instances_by_route.get(&file_item.get_route()) {
|
||||||
|
Some(&id) => id,
|
||||||
|
None => {
|
||||||
|
let id = get_id();
|
||||||
|
instances_by_route.insert(route.clone(), id);
|
||||||
|
|
||||||
|
id
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// This is placeholder logic; this whole function is!
|
||||||
|
let (class_name, property_key, name) = {
|
||||||
|
// TODO: Root instances have an empty route
|
||||||
|
let file_name = route.route.last().unwrap();
|
||||||
|
|
||||||
|
fn strip_suffix<'a>(source: &'a str, suffix: &'static str) -> &'a str {
|
||||||
|
&source[..source.len() - suffix.len()]
|
||||||
|
}
|
||||||
|
|
||||||
|
if file_name.ends_with(".client.lua") {
|
||||||
|
("LocalScript", "Source", strip_suffix(&file_name, ".client.lua"))
|
||||||
|
} else if file_name.ends_with(".server.lua") {
|
||||||
|
("Script", "Source", strip_suffix(&file_name, ".server.lua"))
|
||||||
|
} else if file_name.ends_with(".lua") {
|
||||||
|
("ModuleScript", "Source", strip_suffix(&file_name, ".lua"))
|
||||||
|
} else {
|
||||||
|
// TODO: Error/warn/skip instead of falling back
|
||||||
|
("StringValue", "Value", file_name.as_str())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut properties = HashMap::new();
|
||||||
|
properties.insert(property_key.to_string(), contents.clone());
|
||||||
|
|
||||||
|
tree.insert_instance(primary_id, RbxInstance {
|
||||||
|
name: name.to_string(),
|
||||||
|
class_name: class_name.to_string(),
|
||||||
|
properties,
|
||||||
|
children: Vec::new(),
|
||||||
|
parent: parent_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
(primary_id, vec![primary_id])
|
||||||
|
},
|
||||||
|
FileItem::Directory { children, route } => {
|
||||||
|
let primary_id = match instances_by_route.get(&file_item.get_route()) {
|
||||||
|
Some(&id) => id,
|
||||||
|
None => {
|
||||||
|
let id = get_id();
|
||||||
|
instances_by_route.insert(route.clone(), id);
|
||||||
|
|
||||||
|
id
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut child_ids = Vec::new();
|
||||||
|
|
||||||
|
let mut changed_ids = vec![primary_id];
|
||||||
|
|
||||||
|
for child_file_item in children.values() {
|
||||||
|
let (child_id, mut child_changed_ids) = file_to_instances(child_file_item, partition, tree, instances_by_route, Some(primary_id));
|
||||||
|
|
||||||
|
child_ids.push(child_id);
|
||||||
|
changed_ids.push(child_id);
|
||||||
|
|
||||||
|
// TODO: Should I stop using drain on Vecs of Copyable types?
|
||||||
|
for id in child_changed_ids.drain(..) {
|
||||||
|
changed_ids.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.insert_instance(primary_id, RbxInstance {
|
||||||
|
name: route.name(partition).to_string(),
|
||||||
|
class_name: "Folder".to_string(),
|
||||||
|
properties: HashMap::new(),
|
||||||
|
children: child_ids,
|
||||||
|
parent: parent_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
(primary_id, changed_ids)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RbxSession {
|
||||||
|
project: Project,
|
||||||
|
|
||||||
|
vfs_session: Arc<RwLock<VfsSession>>,
|
||||||
|
|
||||||
|
message_session: MessageSession,
|
||||||
|
|
||||||
|
/// The RbxInstance that represents each partition.
|
||||||
|
// TODO: Can this be removed in favor of instances_by_route?
|
||||||
|
pub partition_instances: HashMap<String, Id>,
|
||||||
|
|
||||||
|
/// Keeps track of all of the instances in the tree
|
||||||
|
pub tree: RbxTree,
|
||||||
|
|
||||||
|
/// A map from files in the VFS to instances loaded in the session.
|
||||||
|
instances_by_route: HashMap<FileRoute, Id>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RbxSession {
|
||||||
|
pub fn new(project: Project, vfs_session: Arc<RwLock<VfsSession>>, message_session: MessageSession) -> RbxSession {
|
||||||
|
RbxSession {
|
||||||
|
project,
|
||||||
|
vfs_session,
|
||||||
|
message_session,
|
||||||
|
partition_instances: HashMap::new(),
|
||||||
|
tree: RbxTree::new(),
|
||||||
|
instances_by_route: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_partitions(&mut self) {
|
||||||
|
let vfs_session_arc = self.vfs_session.clone();
|
||||||
|
let vfs_session = vfs_session_arc.read().unwrap();
|
||||||
|
|
||||||
|
for partition in self.project.partitions.values() {
|
||||||
|
let route = FileRoute {
|
||||||
|
partition: partition.name.clone(),
|
||||||
|
route: Vec::new(),
|
||||||
|
};
|
||||||
|
let file_item = vfs_session.get_by_route(&route).unwrap();
|
||||||
|
|
||||||
|
let parent_id = match route.parent() {
|
||||||
|
Some(parent_route) => match self.instances_by_route.get(&parent_route) {
|
||||||
|
Some(&parent_id) => Some(parent_id),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (root_id, _) = file_to_instances(file_item, partition, &mut self.tree, &mut self.instances_by_route, parent_id);
|
||||||
|
|
||||||
|
self.partition_instances.insert(partition.name.clone(), root_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_change(&mut self, change: &FileChange) {
|
||||||
|
let vfs_session_arc = self.vfs_session.clone();
|
||||||
|
let vfs_session = vfs_session_arc.read().unwrap();
|
||||||
|
|
||||||
|
match change {
|
||||||
|
FileChange::Created(route) | FileChange::Updated(route) => {
|
||||||
|
let file_item = vfs_session.get_by_route(route).unwrap();
|
||||||
|
let partition = self.project.partitions.get(&route.partition).unwrap();
|
||||||
|
|
||||||
|
let parent_id = match route.parent() {
|
||||||
|
Some(parent_route) => match self.instances_by_route.get(&parent_route) {
|
||||||
|
Some(&parent_id) => Some(parent_id),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (_, changed_ids) = file_to_instances(file_item, partition, &mut self.tree, &mut self.instances_by_route, parent_id);
|
||||||
|
|
||||||
|
let messages = changed_ids
|
||||||
|
.iter()
|
||||||
|
.map(|&id| Message::InstanceChanged { id })
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
self.message_session.push_messages(&messages);
|
||||||
|
},
|
||||||
|
FileChange::Deleted(route) => {
|
||||||
|
match self.instances_by_route.get(route) {
|
||||||
|
Some(&id) => {
|
||||||
|
self.tree.delete_instance(id);
|
||||||
|
self.instances_by_route.remove(route);
|
||||||
|
self.message_session.push_messages(&[Message::InstanceChanged { id }]);
|
||||||
|
},
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FileChange::Moved(from_route, to_route) => {
|
||||||
|
let mut messages = Vec::new();
|
||||||
|
|
||||||
|
match self.instances_by_route.get(from_route) {
|
||||||
|
Some(&id) => {
|
||||||
|
self.tree.delete_instance(id);
|
||||||
|
self.instances_by_route.remove(from_route);
|
||||||
|
messages.push(Message::InstanceChanged { id });
|
||||||
|
},
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_item = vfs_session.get_by_route(to_route).unwrap();
|
||||||
|
let partition = self.project.partitions.get(&to_route.partition).unwrap();
|
||||||
|
|
||||||
|
let parent_id = match to_route.parent() {
|
||||||
|
Some(parent_route) => match self.instances_by_route.get(&parent_route) {
|
||||||
|
Some(&parent_id) => Some(parent_id),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (_, changed_ids) = file_to_instances(file_item, partition, &mut self.tree, &mut self.instances_by_route, parent_id);
|
||||||
|
|
||||||
|
for id in changed_ids {
|
||||||
|
messages.push(Message::InstanceChanged { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
self.message_session.push_messages(&messages);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
server/src/session.rs
Normal file
95
server/src/session.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
use std::sync::{mpsc, Arc, RwLock};
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
use message_session::MessageSession;
|
||||||
|
use partition_watcher::PartitionWatcher;
|
||||||
|
use project::Project;
|
||||||
|
use rbx_session::RbxSession;
|
||||||
|
use vfs_session::VfsSession;
|
||||||
|
|
||||||
|
/// Stub trait for middleware
|
||||||
|
trait Middleware {
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Session {
|
||||||
|
pub project: Project,
|
||||||
|
vfs_session: Arc<RwLock<VfsSession>>,
|
||||||
|
rbx_session: Arc<RwLock<RbxSession>>,
|
||||||
|
message_session: MessageSession,
|
||||||
|
watchers: Vec<PartitionWatcher>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
pub fn new(project: Project) -> Session {
|
||||||
|
let message_session = MessageSession::new();
|
||||||
|
let vfs_session = Arc::new(RwLock::new(VfsSession::new(project.clone())));
|
||||||
|
let rbx_session = Arc::new(RwLock::new(RbxSession::new(project.clone(), vfs_session.clone(), message_session.clone())));
|
||||||
|
|
||||||
|
Session {
|
||||||
|
vfs_session,
|
||||||
|
rbx_session,
|
||||||
|
watchers: Vec::new(),
|
||||||
|
message_session,
|
||||||
|
project,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&mut self) {
|
||||||
|
{
|
||||||
|
let mut vfs_session = self.vfs_session.write().unwrap();
|
||||||
|
vfs_session.read_partitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut rbx_session = self.rbx_session.write().unwrap();
|
||||||
|
rbx_session.read_partitions();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
|
for partition in self.project.partitions.values() {
|
||||||
|
let watcher = PartitionWatcher::start_new(partition.clone(), tx.clone());
|
||||||
|
|
||||||
|
self.watchers.push(watcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let vfs_session = self.vfs_session.clone();
|
||||||
|
let rbx_session = self.rbx_session.clone();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
loop {
|
||||||
|
match rx.recv() {
|
||||||
|
Ok(change) => {
|
||||||
|
{
|
||||||
|
let mut vfs_session = vfs_session.write().unwrap();
|
||||||
|
vfs_session.handle_change(&change);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut rbx_session = rbx_session.write().unwrap();
|
||||||
|
rbx_session.handle_change(&change);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(self) {
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_vfs_session(&self) -> Arc<RwLock<VfsSession>> {
|
||||||
|
self.vfs_session.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_rbx_session(&self) -> Arc<RwLock<RbxSession>> {
|
||||||
|
self.rbx_session.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_message_session(&self) -> MessageSession {
|
||||||
|
self.message_session.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
mod vfs_session;
|
|
||||||
mod vfs_item;
|
|
||||||
mod vfs_watcher;
|
|
||||||
|
|
||||||
pub use self::vfs_session::*;
|
|
||||||
pub use self::vfs_item::*;
|
|
||||||
pub use self::vfs_watcher::*;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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>,
|
|
||||||
file_name: String,
|
|
||||||
contents: String,
|
|
||||||
},
|
|
||||||
Dir {
|
|
||||||
route: Vec<String>,
|
|
||||||
file_name: String,
|
|
||||||
children: HashMap<String, VfsItem>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VfsItem {
|
|
||||||
pub fn name(&self) -> &String {
|
|
||||||
match self {
|
|
||||||
&VfsItem::File { ref file_name , .. } => file_name,
|
|
||||||
&VfsItem::Dir { ref file_name , .. } => file_name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn route(&self) -> &[String] {
|
|
||||||
match self {
|
|
||||||
&VfsItem::File { ref route, .. } => route,
|
|
||||||
&VfsItem::Dir { ref route, .. } => route,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
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(_) => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let file_name = path.file_name().unwrap().to_string_lossy().into_owned();
|
|
||||||
|
|
||||||
Ok(VfsItem::Dir {
|
|
||||||
route: route.iter().cloned().collect::<Vec<_>>(),
|
|
||||||
file_name,
|
|
||||||
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(()),
|
|
||||||
}
|
|
||||||
|
|
||||||
let file_name = path.file_name().unwrap().to_string_lossy().into_owned();
|
|
||||||
|
|
||||||
Ok(VfsItem::File {
|
|
||||||
route: route.iter().cloned().collect::<Vec<_>>(),
|
|
||||||
file_name,
|
|
||||||
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!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
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_millis(200))
|
|
||||||
.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
242
server/src/vfs_session.rs
Normal file
242
server/src/vfs_session.rs
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
use file_route::FileRoute;
|
||||||
|
use project::Project;
|
||||||
|
|
||||||
|
/// 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<String, FileItem>,
|
||||||
|
route: FileRoute,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileItem {
|
||||||
|
pub fn get_route(&self) -> &FileRoute {
|
||||||
|
match self {
|
||||||
|
FileItem::File { route, .. } => route,
|
||||||
|
FileItem::Directory { route, .. } => route,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum FileChange {
|
||||||
|
Created(FileRoute),
|
||||||
|
Deleted(FileRoute),
|
||||||
|
Updated(FileRoute),
|
||||||
|
Moved(FileRoute, FileRoute),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct VfsSession {
|
||||||
|
pub project: Project,
|
||||||
|
|
||||||
|
/// The in-memory files associated with each partition.
|
||||||
|
pub partition_files: HashMap<String, FileItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VfsSession {
|
||||||
|
pub fn new(project: Project) -> VfsSession {
|
||||||
|
VfsSession {
|
||||||
|
project,
|
||||||
|
partition_files: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_partitions(&mut self) {
|
||||||
|
for partition_name in self.project.partitions.keys() {
|
||||||
|
let route = FileRoute {
|
||||||
|
partition: partition_name.clone(),
|
||||||
|
route: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let file_item = self.read(&route).expect("Couldn't load partitions");
|
||||||
|
|
||||||
|
self.partition_files.insert(partition_name.clone(), file_item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_change(&mut self, change: &FileChange) -> Option<()> {
|
||||||
|
match change {
|
||||||
|
FileChange::Created(route) | FileChange::Updated(route) => {
|
||||||
|
let new_item = self.read(&route).ok()?;
|
||||||
|
self.set_file_item(new_item);
|
||||||
|
},
|
||||||
|
FileChange::Deleted(route) => {
|
||||||
|
self.delete_route(&route);
|
||||||
|
},
|
||||||
|
FileChange::Moved(from_route, to_route) => {
|
||||||
|
let new_item = self.read(&to_route).ok()?;
|
||||||
|
self.delete_route(&from_route);
|
||||||
|
self.set_file_item(new_item);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_by_route(&self, route: &FileRoute) -> Option<&FileItem> {
|
||||||
|
let partition = self.partition_files.get(&route.partition)?;
|
||||||
|
let mut current = partition;
|
||||||
|
|
||||||
|
for piece in &route.route {
|
||||||
|
match current {
|
||||||
|
FileItem::File { .. } => return None,
|
||||||
|
FileItem::Directory { children, .. } => {
|
||||||
|
current = children.get(piece)?;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_by_route_mut(&mut self, route: &FileRoute) -> Option<&mut FileItem> {
|
||||||
|
let mut current = self.partition_files.get_mut(&route.partition)?;
|
||||||
|
|
||||||
|
for piece in &route.route {
|
||||||
|
let mut next = match { current } {
|
||||||
|
FileItem::File { .. } => return None,
|
||||||
|
FileItem::Directory { children, .. } => {
|
||||||
|
children.get_mut(piece)?
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_file_item(&mut self, item: FileItem) {
|
||||||
|
match self.get_by_route_mut(item.get_route()) {
|
||||||
|
Some(existing) => {
|
||||||
|
mem::replace(existing, item);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
None => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.get_route().route.len() > 0 {
|
||||||
|
let mut parent_route = item.get_route().clone();
|
||||||
|
let child_name = parent_route.route.pop().unwrap();
|
||||||
|
|
||||||
|
let mut parent_children = HashMap::new();
|
||||||
|
parent_children.insert(child_name, item);
|
||||||
|
|
||||||
|
let parent_item = FileItem::Directory {
|
||||||
|
route: parent_route,
|
||||||
|
children: parent_children,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.set_file_item(parent_item);
|
||||||
|
} else {
|
||||||
|
self.partition_files.insert(item.get_route().partition.clone(), item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_route(&mut self, route: &FileRoute) -> Option<()> {
|
||||||
|
if route.route.len() == 0 {
|
||||||
|
self.partition_files.remove(&route.partition);
|
||||||
|
return Some(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut current = self.partition_files.get_mut(&route.partition)?;
|
||||||
|
|
||||||
|
for i in 0..(route.route.len() - 1) {
|
||||||
|
let piece = &route.route[i];
|
||||||
|
|
||||||
|
let mut next = match { current } {
|
||||||
|
FileItem::File { .. } => return None,
|
||||||
|
FileItem::Directory { children, .. } => {
|
||||||
|
children.get_mut(piece)?
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
match current {
|
||||||
|
FileItem::Directory { children, .. } => {
|
||||||
|
children.remove(route.route.last().unwrap().as_str());
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(&self, route: &FileRoute) -> Result<FileItem, ()> {
|
||||||
|
let partition_path = &self.project.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<FileItem, ()> {
|
||||||
|
let partition_path = &self.project.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<FileItem, ()> {
|
||||||
|
let partition_path = &self.project.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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,225 +1,212 @@
|
|||||||
use std::io::Read;
|
use std::borrow::Cow;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{mpsc, RwLock, Arc};
|
||||||
|
|
||||||
use rouille;
|
use rouille::{self, Request, Response};
|
||||||
use serde;
|
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
|
use id::Id;
|
||||||
|
use message_session::{MessageSession, Message};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use vfs::{VfsSession, VfsChange};
|
|
||||||
use rbx::RbxInstance;
|
use rbx::RbxInstance;
|
||||||
use plugin::PluginChain;
|
use rbx_session::RbxSession;
|
||||||
|
use session::Session;
|
||||||
static MAX_BODY_SIZE: usize = 25 * 1024 * 1024; // 25 MiB
|
|
||||||
|
|
||||||
/// The set of configuration the web server needs to start.
|
/// The set of configuration the web server needs to start.
|
||||||
pub struct WebConfig {
|
pub struct WebConfig {
|
||||||
pub port: u64,
|
pub port: u64,
|
||||||
pub verbose: bool,
|
pub project: Project,
|
||||||
pub server_id: u64,
|
pub server_id: u64,
|
||||||
|
pub rbx_session: Arc<RwLock<RbxSession>>,
|
||||||
|
pub message_session: MessageSession,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
impl WebConfig {
|
||||||
|
pub fn from_session(server_id: u64, port: u64, session: &Session) -> WebConfig {
|
||||||
|
WebConfig {
|
||||||
|
port,
|
||||||
|
server_id,
|
||||||
|
project: session.project.clone(),
|
||||||
|
rbx_session: session.get_rbx_session(),
|
||||||
|
message_session: session.get_message_session(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct ServerInfo<'a> {
|
pub struct ServerInfoResponse<'a> {
|
||||||
|
pub server_id: &'a str,
|
||||||
|
pub server_version: &'a str,
|
||||||
|
pub protocol_version: u64,
|
||||||
|
pub partitions: HashMap<String, Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ReadAllResponse<'a> {
|
||||||
|
pub server_id: &'a str,
|
||||||
|
pub message_cursor: i32,
|
||||||
|
pub instances: Cow<'a, HashMap<Id, RbxInstance>>,
|
||||||
|
pub partition_instances: Cow<'a, HashMap<String, Id>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ReadResponse<'a> {
|
||||||
|
pub server_id: &'a str,
|
||||||
|
pub message_cursor: i32,
|
||||||
|
pub instances: HashMap<Id, Cow<'a, RbxInstance>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SubscribeResponse<'a> {
|
||||||
|
pub server_id: &'a str,
|
||||||
|
pub message_cursor: i32,
|
||||||
|
pub messages: Cow<'a, [Message]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Server {
|
||||||
|
config: WebConfig,
|
||||||
server_version: &'static str,
|
server_version: &'static str,
|
||||||
protocol_version: u64,
|
server_id: String,
|
||||||
server_id: &'a str,
|
|
||||||
project: &'a Project,
|
|
||||||
current_time: f64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
impl Server {
|
||||||
#[serde(rename_all = "camelCase")]
|
pub fn new(config: WebConfig) -> Server {
|
||||||
struct ReadResult<'a> {
|
Server {
|
||||||
items: Vec<Option<RbxInstance>>,
|
server_version: env!("CARGO_PKG_VERSION"),
|
||||||
server_id: &'a str,
|
server_id: config.server_id.to_string(),
|
||||||
current_time: f64,
|
config,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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() {
|
pub fn handle_request(&self, request: &Request) -> Response {
|
||||||
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,
|
router!(request,
|
||||||
(GET) (/) => {
|
(GET) (/) => {
|
||||||
|
Response::text("Rojo up and running!")
|
||||||
|
},
|
||||||
|
|
||||||
|
(GET) (/api/rojo) => {
|
||||||
// Get a summary of information about the server.
|
// Get a summary of information about the server.
|
||||||
|
|
||||||
let current_time = {
|
let mut partitions = HashMap::new();
|
||||||
let vfs = vfs.lock().unwrap();
|
|
||||||
|
|
||||||
vfs.current_time()
|
for partition in self.config.project.partitions.values() {
|
||||||
};
|
partitions.insert(partition.name.clone(), partition.target.clone());
|
||||||
|
|
||||||
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 {
|
Response::json(&ServerInfoResponse {
|
||||||
server_id: &server_id,
|
server_version: self.server_version,
|
||||||
items: rbx_items,
|
protocol_version: 2,
|
||||||
current_time,
|
server_id: &self.server_id,
|
||||||
|
partitions: partitions,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
(POST) (/write) => {
|
(GET) (/api/subscribe/{ cursor: i32 }) => {
|
||||||
// Not yet implemented.
|
// Retrieve any messages past the given cursor index, and if
|
||||||
|
// there weren't any, subscribe to receive any new messages.
|
||||||
|
|
||||||
let _write_request: Vec<WriteSpecifier> = match read_json(&request) {
|
// Did the client miss any messages since the last subscribe?
|
||||||
Some(v) => v,
|
{
|
||||||
None => return rouille::Response::empty_400(),
|
let messages = self.config.message_session.messages.read().unwrap();
|
||||||
};
|
|
||||||
|
|
||||||
rouille::Response::empty_404()
|
if cursor > messages.len() as i32 {
|
||||||
|
return Response::json(&SubscribeResponse {
|
||||||
|
server_id: &self.server_id,
|
||||||
|
messages: Cow::Borrowed(&[]),
|
||||||
|
message_cursor: messages.len() as i32 - 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if cursor < messages.len() as i32 - 1 {
|
||||||
|
let new_messages = &messages[(cursor + 1) as usize..];
|
||||||
|
let new_cursor = cursor + new_messages.len() as i32;
|
||||||
|
|
||||||
|
return Response::json(&SubscribeResponse {
|
||||||
|
server_id: &self.server_id,
|
||||||
|
messages: Cow::Borrowed(new_messages),
|
||||||
|
message_cursor: new_cursor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
|
let sender_id = self.config.message_session.subscribe(tx);
|
||||||
|
|
||||||
|
match rx.recv() {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(_) => return Response::text("error!").with_status_code(500),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.config.message_session.unsubscribe(sender_id);
|
||||||
|
|
||||||
|
{
|
||||||
|
let messages = self.config.message_session.messages.read().unwrap();
|
||||||
|
let new_messages = &messages[(cursor + 1) as usize..];
|
||||||
|
let new_cursor = cursor + new_messages.len() as i32;
|
||||||
|
|
||||||
|
Response::json(&SubscribeResponse {
|
||||||
|
server_id: &self.server_id,
|
||||||
|
messages: Cow::Borrowed(new_messages),
|
||||||
|
message_cursor: new_cursor,
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_ => rouille::Response::empty_404()
|
(GET) (/api/read_all) => {
|
||||||
|
let rbx_session = self.config.rbx_session.read().unwrap();
|
||||||
|
|
||||||
|
let message_cursor = self.config.message_session.get_message_cursor();
|
||||||
|
|
||||||
|
Response::json(&ReadAllResponse {
|
||||||
|
server_id: &self.server_id,
|
||||||
|
message_cursor,
|
||||||
|
instances: Cow::Borrowed(rbx_session.tree.get_all_instances()),
|
||||||
|
partition_instances: Cow::Borrowed(&rbx_session.partition_instances),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
(GET) (/api/read/{ id_list: String }) => {
|
||||||
|
let requested_ids = id_list
|
||||||
|
.split(",")
|
||||||
|
.map(str::parse::<Id>)
|
||||||
|
.collect::<Result<Vec<Id>, _>>();
|
||||||
|
|
||||||
|
let requested_ids = match requested_ids {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return rouille::Response::text("Malformed ID list").with_status_code(400),
|
||||||
|
};
|
||||||
|
|
||||||
|
let rbx_session = self.config.rbx_session.read().unwrap();
|
||||||
|
|
||||||
|
let message_cursor = self.config.message_session.get_message_cursor();
|
||||||
|
|
||||||
|
let mut instances = HashMap::new();
|
||||||
|
|
||||||
|
for requested_id in &requested_ids {
|
||||||
|
rbx_session.tree.get_instance(*requested_id, &mut instances);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::json(&ReadResponse {
|
||||||
|
server_id: &self.server_id,
|
||||||
|
message_cursor,
|
||||||
|
instances,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => Response::empty_404()
|
||||||
)
|
)
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the Rojo web server, taking over the current thread.
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
pub fn start(config: WebConfig) {
|
||||||
|
let address = format!("localhost:{}", config.port);
|
||||||
|
let server = Server::new(config);
|
||||||
|
|
||||||
|
rouille::start_server(address, move |request| server.handle_request(request));
|
||||||
}
|
}
|
||||||
|
|||||||
43
server/src/web_util.rs
Normal file
43
server/src/web_util.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use rouille;
|
||||||
|
use serde;
|
||||||
|
use serde_json;
|
||||||
|
|
||||||
|
static MAX_BODY_SIZE: usize = 100 * 1024 * 1024; // 100 MiB
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
let content_type = request.header("Content-Type")?;
|
||||||
|
|
||||||
|
if !content_type.starts_with("application/json") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = request.data()?;
|
||||||
|
|
||||||
|
// Allocate a buffer and read up to MAX_BODY_SIZE+1 bytes into it.
|
||||||
|
let mut out = Vec::new();
|
||||||
|
body.take(MAX_BODY_SIZE.saturating_add(1) as u64).read_to_end(&mut out).ok()?;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
String::from_utf8(out).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the body out of a Rouille Request and attempts to turn it into JSON.
|
||||||
|
pub fn read_json<T>(request: &rouille::Request) -> Option<T>
|
||||||
|
where
|
||||||
|
T: serde::de::DeserializeOwned,
|
||||||
|
{
|
||||||
|
let body = read_json_text(&request)?;
|
||||||
|
serde_json::from_str(&body).ok()?
|
||||||
|
}
|
||||||
5
server/test-projects/empty/rojo.json
Normal file
5
server/test-projects/empty/rojo.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "empty",
|
||||||
|
"servePort": 23456,
|
||||||
|
"partitions": {}
|
||||||
|
}
|
||||||
1
server/test-projects/one-partition/lib/a.lua
Normal file
1
server/test-projects/one-partition/lib/a.lua
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-- a.lua
|
||||||
1
server/test-projects/one-partition/lib/a.server.lua
Normal file
1
server/test-projects/one-partition/lib/a.server.lua
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-- a.server.lua
|
||||||
1
server/test-projects/one-partition/lib/b.client.lua
Normal file
1
server/test-projects/one-partition/lib/b.client.lua
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-- b.client.lua
|
||||||
10
server/test-projects/one-partition/rojo.json
Normal file
10
server/test-projects/one-partition/rojo.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "one-partition",
|
||||||
|
"servePort": 23456,
|
||||||
|
"partitions": {
|
||||||
|
"lib": {
|
||||||
|
"path": "lib",
|
||||||
|
"target": "ReplicatedStorage.OnePartition"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
server/tests/test_util/mod.rs
Normal file
22
server/tests/test_util/mod.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use rouille::Request;
|
||||||
|
|
||||||
|
use librojo::web::Server;
|
||||||
|
|
||||||
|
pub trait HttpTestUtil {
|
||||||
|
fn get_string(&self, url: &str) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpTestUtil for Server {
|
||||||
|
fn get_string(&self, url: &str) -> String {
|
||||||
|
let info_request = Request::fake_http("GET", url, vec![], vec![]);
|
||||||
|
let response = self.handle_request(&info_request);
|
||||||
|
|
||||||
|
assert_eq!(response.status_code, 200);
|
||||||
|
|
||||||
|
let (mut reader, _) = response.data.into_reader_and_size();
|
||||||
|
let mut body = String::new();
|
||||||
|
reader.read_to_string(&mut body).unwrap();
|
||||||
|
|
||||||
|
body
|
||||||
|
}
|
||||||
|
}
|
||||||
170
server/tests/web.rs
Normal file
170
server/tests/web.rs
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
extern crate rouille;
|
||||||
|
extern crate serde_json;
|
||||||
|
extern crate serde;
|
||||||
|
|
||||||
|
extern crate librojo;
|
||||||
|
|
||||||
|
mod test_util;
|
||||||
|
use test_util::*;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use librojo::{
|
||||||
|
session::Session,
|
||||||
|
project::Project,
|
||||||
|
web::{Server, WebConfig, ServerInfoResponse, ReadResponse, ReadAllResponse},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty() {
|
||||||
|
let project_path = {
|
||||||
|
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
path.push("test-projects/empty");
|
||||||
|
path
|
||||||
|
};
|
||||||
|
|
||||||
|
let project = Project::load(&project_path).unwrap();
|
||||||
|
let mut session = Session::new(project.clone());
|
||||||
|
session.start();
|
||||||
|
|
||||||
|
let web_config = WebConfig::from_session(0, project.serve_port, &session);
|
||||||
|
let server = Server::new(web_config);
|
||||||
|
|
||||||
|
{
|
||||||
|
let body = server.get_string("/api/rojo");
|
||||||
|
let response = serde_json::from_str::<ServerInfoResponse>(&body).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.server_id, "0");
|
||||||
|
assert_eq!(response.protocol_version, 2);
|
||||||
|
assert_eq!(response.partitions.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let body = server.get_string("/api/read_all");
|
||||||
|
let response = serde_json::from_str::<ReadAllResponse>(&body).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.server_id, "0");
|
||||||
|
assert_eq!(response.message_cursor, -1);
|
||||||
|
assert_eq!(response.instances.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let body = server.get_string("/api/read/0");
|
||||||
|
let response = serde_json::from_str::<ReadResponse>(&body).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.server_id, "0");
|
||||||
|
assert_eq!(response.message_cursor, -1);
|
||||||
|
assert_eq!(response.instances.len(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn one_partition() {
|
||||||
|
let project_path = {
|
||||||
|
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
path.push("test-projects/one-partition");
|
||||||
|
path
|
||||||
|
};
|
||||||
|
|
||||||
|
let project = Project::load(&project_path).unwrap();
|
||||||
|
let mut session = Session::new(project.clone());
|
||||||
|
session.start();
|
||||||
|
|
||||||
|
let web_config = WebConfig::from_session(0, project.serve_port, &session);
|
||||||
|
let server = Server::new(web_config);
|
||||||
|
|
||||||
|
{
|
||||||
|
let body = server.get_string("/api/rojo");
|
||||||
|
let response = serde_json::from_str::<ServerInfoResponse>(&body).unwrap();
|
||||||
|
|
||||||
|
let mut partitions = HashMap::new();
|
||||||
|
partitions.insert("lib".to_string(), vec!["ReplicatedStorage".to_string(), "OnePartition".to_string()]);
|
||||||
|
|
||||||
|
assert_eq!(response.server_id, "0");
|
||||||
|
assert_eq!(response.protocol_version, 2);
|
||||||
|
assert_eq!(response.partitions, partitions);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let body = server.get_string("/api/read_all");
|
||||||
|
let response = serde_json::from_str::<ReadAllResponse>(&body).unwrap();
|
||||||
|
|
||||||
|
let partition_id = *response.partition_instances.get("lib").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.server_id, "0");
|
||||||
|
assert_eq!(response.message_cursor, -1);
|
||||||
|
assert_eq!(response.instances.len(), 4); // root and three children
|
||||||
|
|
||||||
|
let mut found_root = false;
|
||||||
|
let mut found_module = false;
|
||||||
|
let mut found_client = false;
|
||||||
|
let mut found_server = false;
|
||||||
|
|
||||||
|
for (id, instance) in response.instances.iter() {
|
||||||
|
match instance.class_name.as_str() {
|
||||||
|
// TOOD: Should partition roots (and other directories) be some
|
||||||
|
// magical object instead of Folder?
|
||||||
|
"Folder" => {
|
||||||
|
assert!(!found_root);
|
||||||
|
found_root = true;
|
||||||
|
|
||||||
|
assert_eq!(*id, partition_id);
|
||||||
|
|
||||||
|
// TODO: Should this actually equal the last part of the
|
||||||
|
// partition's target?
|
||||||
|
assert_eq!(instance.name, "OnePartition");
|
||||||
|
|
||||||
|
assert_eq!(instance.properties.len(), 0);
|
||||||
|
assert_eq!(instance.parent, None);
|
||||||
|
assert_eq!(instance.children.len(), 3);
|
||||||
|
},
|
||||||
|
"ModuleScript" => {
|
||||||
|
assert!(!found_module);
|
||||||
|
found_module = true;
|
||||||
|
|
||||||
|
let mut properties = HashMap::new();
|
||||||
|
properties.insert("Source".to_string(), "-- a.lua".to_string());
|
||||||
|
|
||||||
|
assert_eq!(instance.name, "a");
|
||||||
|
assert_eq!(instance.properties, properties);
|
||||||
|
assert_eq!(instance.parent, Some(partition_id));
|
||||||
|
assert_eq!(instance.children.len(), 0);
|
||||||
|
},
|
||||||
|
"LocalScript" => {
|
||||||
|
assert!(!found_client);
|
||||||
|
found_client = true;
|
||||||
|
|
||||||
|
let mut properties = HashMap::new();
|
||||||
|
properties.insert("Source".to_string(), "-- b.client.lua".to_string());
|
||||||
|
|
||||||
|
assert_eq!(instance.name, "b");
|
||||||
|
assert_eq!(instance.properties, properties);
|
||||||
|
assert_eq!(instance.parent, Some(partition_id));
|
||||||
|
assert_eq!(instance.children.len(), 0);
|
||||||
|
},
|
||||||
|
"Script" => {
|
||||||
|
assert!(!found_server);
|
||||||
|
found_server = true;
|
||||||
|
|
||||||
|
let mut properties = HashMap::new();
|
||||||
|
properties.insert("Source".to_string(), "-- a.server.lua".to_string());
|
||||||
|
|
||||||
|
assert_eq!(instance.name, "a");
|
||||||
|
assert_eq!(instance.properties, properties);
|
||||||
|
assert_eq!(instance.parent, Some(partition_id));
|
||||||
|
assert_eq!(instance.children.len(), 0);
|
||||||
|
},
|
||||||
|
_ => panic!("Unexpected instance!"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(found_root);
|
||||||
|
assert!(found_module);
|
||||||
|
assert!(found_client);
|
||||||
|
assert!(found_server);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Test /read
|
||||||
|
// TODO: Test /subscribe
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user