diff --git a/.gitignore b/.gitignore index eb5a316cb..220040f55 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ target +*.css +*.css.map +.sass-cache diff --git a/Cargo.lock b/Cargo.lock index fbee77a8d..523269c46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,16 +6,26 @@ dependencies = [ "clap 2.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "git2 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", + "handlebars-iron 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.9.7 (registry+https://github.com/rust-lang/crates.io-index)", + "iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "magic 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "params 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "postgres 0.11.8 (registry+https://github.com/rust-lang/crates.io-index)", + "pulldown-cmark 0.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "r2d2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "r2d2_postgres 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.71 (registry+https://github.com/rust-lang/crates.io-index)", + "router 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "slug 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "staticfile 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -60,6 +70,23 @@ name = "bitflags" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "bodyparser" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "persistent 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "buf_redux" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "bufstream" version = "0.1.2" @@ -129,6 +156,14 @@ dependencies = [ "gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "conduit-mime-types" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "cookie" version = "0.2.5" @@ -196,6 +231,15 @@ dependencies = [ "regex 0.1.71 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "error" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "traitobject 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "filetime" version = "0.1.10" @@ -237,6 +281,11 @@ dependencies = [ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "getopts" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "git2" version = "0.4.3" @@ -264,6 +313,32 @@ name = "glob" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "handlebars" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "itertools 0.4.15 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "quick-error 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.71 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "handlebars-iron" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "handlebars 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)", + "iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "walkdir 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "hex" version = "0.2.0" @@ -282,6 +357,27 @@ name = "httparse" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "hyper" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cookie 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "solicit 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", + "traitobject 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "url 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "hyper" version = "0.9.7" @@ -314,6 +410,28 @@ dependencies = [ "unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "iron" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "conduit-mime-types 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "error 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "modifier 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", + "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "url 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "itertools" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -328,6 +446,11 @@ name = "language-tags" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "lazy_static" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "lazy_static" version = "0.2.1" @@ -431,6 +554,17 @@ dependencies = [ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "mime_guess" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "phf 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_codegen 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "miniz-sys" version = "0.1.7" @@ -440,6 +574,36 @@ dependencies = [ "libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "modifier" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "mount" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "sequence_trie 0.0.13 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "multipart" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "buf_redux 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "mime_guess 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "tempdir 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "net2" version = "0.2.23" @@ -457,6 +621,72 @@ name = "nom" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "num" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-bigint 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-complex 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-iter 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-rational 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-bigint" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-complex" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-integer" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-iter" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-rational" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-bigint 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "num_cpus" version = "0.2.12" @@ -508,6 +738,29 @@ dependencies = [ "openssl 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "params" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bodyparser 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "multipart 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "urlencoded 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "persistent" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "phf" version = "0.7.14" @@ -516,16 +769,45 @@ dependencies = [ "phf_shared 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "phf_codegen" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_generator 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_shared 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf_generator" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_shared 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "phf_shared" version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] name = "pkg-config" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "plugin" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "pnacl-build-helper" version = "1.4.10" @@ -549,6 +831,37 @@ dependencies = [ "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "pulldown-cmark" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quick-error" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "r2d2" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "r2d2_postgres" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "postgres 0.11.8 (registry+https://github.com/rust-lang/crates.io-index)", + "r2d2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rand" version = "0.3.14" @@ -574,6 +887,20 @@ name = "regex-syntax" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "route-recognizer" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "router" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "route-recognizer 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rustc-serialize" version = "0.3.19" @@ -600,6 +927,25 @@ dependencies = [ "nom 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "sequence_trie" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_json" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "slug" version = "0.1.1" @@ -617,6 +963,19 @@ dependencies = [ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "staticfile" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "filetime 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "mount 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", + "url 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "strsim" version = "0.3.0" @@ -693,11 +1052,24 @@ name = "traitobject" version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "traitobject" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "typeable" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "typemap" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unsafe-any 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "unicase" version = "1.4.0" @@ -729,6 +1101,36 @@ name = "unidecode" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "unsafe-any" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "traitobject 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "url" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "url" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "url" version = "1.1.1" @@ -738,6 +1140,17 @@ dependencies = [ "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "urlencoded" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bodyparser 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "url 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "user32-sys" version = "0.2.0" @@ -752,11 +1165,37 @@ name = "utf8-ranges" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "uuid" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "uuid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "vec_map" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "walkdir" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "winapi" version = "0.2.7" @@ -776,3 +1215,143 @@ dependencies = [ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[metadata] +"checksum advapi32-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "307c92332867e586720c0222ee9d890bbe8431711efed8a1b06bc5b40fc66bd7" +"checksum aho-corasick 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2b3fb52b09c1710b961acb35390d514be82e4ac96a9969a8e38565a29b878dc9" +"checksum ansi_term 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1f46cd5b1d660c938e3f92dfe7a73d832b3281479363dd0cd9c1c2fbf60f7962" +"checksum bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2a6577517ecd0ee0934f48a7295a89aaef3e6dfafeac404f94c0b3448518ddfe" +"checksum bitflags 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "32866f4d103c4e438b1db1158aa1b1a80ee078e5d77a59a2f906fd62a577389c" +"checksum bitflags 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4f67931368edf3a9a51d29886d245f1c3db2f1ef0dcc9e35ff70341b78c10d23" +"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" +"checksum bodyparser 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "61b8de7c9850ace6754449f7bed9869771a84f6f3089281aaea6d521a684f6d3" +"checksum buf_redux 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b115bd9935c68b58f80ff867e1c46942c4aed79e78bcc8c2bc22d50f52bb9099" +"checksum bufstream 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7b48dbe2ff0e98fa2f03377d204a9637d3c9816cd431bfe05a8abbd0ea11d074" +"checksum byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0fc10e8cc6b2580fda3f36eb6dc5316657f812a3df879a44a66fc9f0fdbc4855" +"checksum cargo 0.12.0 (git+https://github.com/rust-lang/cargo.git?rev=ffa147d393037fd5632cd8c9d048b5521d70ab4c)" = "" +"checksum cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de1e760d7b6535af4241fca8bd8adf68e2e7edacc6b29f5d399050c5e48cf88c" +"checksum clap 2.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "96fee724ff92564914e8aa708919449e0c7b165f7833110b4b1ade9b3a9b17e8" +"checksum cmake 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "dfcf5bcece56ef953b8ea042509e9dcbdfe97820b7e20d86beb53df30ed94978" +"checksum conduit-mime-types 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "95ca30253581af809925ef68c2641cc140d6183f43e12e0af4992d53768bd7b8" +"checksum cookie 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0e3d6405328b6edb412158b3b7710e2634e23f3614b9bb1c412df7952489a626" +"checksum crates-io 0.4.0 (git+https://github.com/rust-lang/cargo.git?rev=ffa147d393037fd5632cd8c9d048b5521d70ab4c)" = "" +"checksum crossbeam 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "fb974f835e90390c5f9dfac00f05b06dc117299f5ea4e85fbc7bb443af4911cc" +"checksum curl 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "26fa2377bacffb278a472dcc2c742577316527d2a8ce588455127c6ff4521846" +"checksum curl-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "780c1e295903f12cb0598d73703f850615f685eeeb4f2323fbd2911ef337da7e" +"checksum docopt 0.6.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4cc0acb4ce0828c6a5a11d47baa432fe885881c27428c3a4e473e454ffe57a76" +"checksum env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "aba65b63ffcc17ffacd6cf5aa843da7c5a25e3bd4bbe0b7def8b214e411250e5" +"checksum error 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "a6e606f14042bb87cc02ef6a14db6c90ab92ed6f62d87e69377bc759fd7987cc" +"checksum filetime 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "5363ab8e4139b8568a6237db5248646e5a8a2f89bd5ccb02092182b11fd3e922" +"checksum flate2 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "3eeb481e957304178d2e782f2da1257f1434dfecbae883bafb61ada2a9fea3bb" +"checksum fs2 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "66fd7974e97e3c47dc89c146a05c6e8e90c4267daee9e75c229d4e049dce454d" +"checksum gcc 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)" = "3da3a2cbaeb01363c8e3704fd9fd0eb2ceb17c6f27abd4c1ef040fb57d20dc79" +"checksum gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0912515a8ff24ba900422ecda800b52f4016a56251922d397c576bf92c690518" +"checksum getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9047cfbd08a437050b363d35ef160452c5fe8ea5187ae0a624708c91581d685" +"checksum git2 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c4f04d03627ebb8eac2b4e5f3411041583e86338ab98fcbb8f1c8e74d1f5eef7" +"checksum git2-curl 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d5f766d804e3cf2b90e16ab77c3ddedcb1ca5d2456cadb7b3f907345f8c3498" +"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" +"checksum handlebars 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf35fba54316319d55e8f97eedb97cf19484ad22bde6a312f141d32e25fb6855" +"checksum handlebars-iron 0.15.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2766bb9c09bffd3e4553e0f47b1c40d6a084c2e0166f8dfb11514cb550e28efc" +"checksum hex 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d6a22814455d41612f41161581c2883c0c6a1c41852729b17d5ed88f01e153aa" +"checksum hpack 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d2da7d3a34cf6406d9d700111b8eafafe9a251de41ae71d8052748259343b58" +"checksum httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "46534074dbb80b070d60a5cb8ecadd8963a00a438ae1a95268850a7ef73b67ae" +"checksum hyper 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bb0f4d00bb781e559b6e66ae4b5479df0fdf9ab15949f52fa2f1f5de16d4cc07" +"checksum hyper 0.9.7 (registry+https://github.com/rust-lang/crates.io-index)" = "e7007ac7992a1fec4cc3a486888e8280b7b87bc8cc548a74a8634ecac1f70871" +"checksum idna 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1053236e00ce4f668aeca4a769a09b3bf5a682d802abd6f3cb39374f6b162c11" +"checksum iron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bce8d64fae23c51acaaa737720b3dde37aeacd3d21ea73059999e0016c9673d9" +"checksum itertools 0.4.15 (registry+https://github.com/rust-lang/crates.io-index)" = "88f21fed5ebd96f4db04106cd37c21417393e08533d3914a42fc666f75d5064f" +"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" +"checksum lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "cf186d1a8aa5f5bee5fd662bc9c1b949e0259e1bcc379d1f006847b0080c7417" +"checksum lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "49247ec2a285bb3dcb23cbd9c35193c025e7251bfce77c1d5da97e6362dffe7f" +"checksum libc 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "e32a70cf75e5846d53a673923498228bbec6a8624708a9ea5645f075d6276122" +"checksum libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "97def9dc7ce1d8e153e693e3a33020bc69972181adb2f871e87e888876feae49" +"checksum libgit2-sys 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "dc885e2d96f8df06bff0814e6a50a9e93af7ab188474c43d3770ca204e4c8202" +"checksum libressl-pnacl-sys 2.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "cbc058951ab6a3ef35ca16462d7642c4867e6403520811f28537a4e2f2db3e71" +"checksum libssh2-sys 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "c45fba84ee1fa05b830cb471741ef30d41eb1c3b97160b8ad8d955af824de880" +"checksum libz-sys 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c9795a8a0498b3abab873f8f063816fcc2e002388e89df87da065628dd5a8ed2" +"checksum log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ab83497bf8bf4ed2a74259c1c802351fcd67a65baa86394b6ba73c36f4838054" +"checksum magic 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0171890abdef5ff5e9199aa40831d71ae43f08ffb63504a9544a69e9c3e38945" +"checksum magic-sys 0.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "255aea8397b20a4779dea4656428f6df29d19ff4d679bcd0fac2fc3aa802ea5a" +"checksum matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "15305656809ce5a4805b1ff2946892810992197ce1270ff79baded852187942e" +"checksum memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20" +"checksum mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a74cc2587bf97c49f3f5bab62860d6abf3902ca73b66b51d9b049fbdcd727bd2" +"checksum mime_guess 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e9a7d89cb3bce9145b0d0339a0588b044e3e3e3faa1dcd74822ebdc36bfac020" +"checksum miniz-sys 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "9d1f4d337a01c32e1f2122510fed46393d53ca35a7f429cb0450abaedfa3ed54" +"checksum modifier 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "41f5c9112cb662acd3b204077e0de5bc66305fa8df65c8019d5adb10e9ab6e58" +"checksum mount 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9098630c44ff3511087d2f06889141a98d632a62d7745abb132d8e08b325dd39" +"checksum multipart 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6afda429f7ebcb31c1ce8ea4c6afb2bec0b3f28b002686610b29f323e92a2731" +"checksum net2 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)" = "6a816012ca11cb47009693c1e0c6130e26d39e4d97ee2a13c50e868ec83e3204" +"checksum nom 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d1b06a35295796400a1db7382054f93713bf3924e7c268af94c5357b9fbf4cb6" +"checksum num 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "c04bd954dbf96f76bab6e5bd6cef6f1ce1262d15268ce4f926d2b5b778fa7af2" +"checksum num-bigint 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "41655c8d667be847a0b72fe0888857a7b3f052f691cf40852be5fcf87b274a65" +"checksum num-complex 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "ccac67baf893ac97474f8d70eff7761dabb1f6c66e71f8f1c67a6859218db810" +"checksum num-integer 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "fb24d9bfb3f222010df27995441ded1e954f8f69cd35021f6bef02ca9552fb92" +"checksum num-iter 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "287a1c9969a847055e1122ec0ea7a5c5d6f72aad97934e131c83d5c08ab4e45c" +"checksum num-rational 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "48cdcc9ff4ae2a8296805ac15af88b3d88ce62128ded0cb74ffb63a587502a84" +"checksum num-traits 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "51eab148f171aefad295f8cece636fc488b9b392ef544da31ea4b8ef6b9e9c39" +"checksum num_cpus 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "ffa54f34bd652b17a0c872a9878688196f1942a87f9917e117a5bdad8092dedb" +"checksum openssl 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)" = "81ff0208f23e726e747375d34e40c93d037a5b504de7305117dfe5ad72516d2d" +"checksum openssl-sys 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)" = "618753feb53784e3ccb131811ed0b02f80640da89fb33b165d69146564b02085" +"checksum openssl-sys-extras 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)" = "01838027da8e31ab4d3530fc5d6752bfd92dcc8e0ae070633e69f2b020bd0f36" +"checksum openssl-verify 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ed86cce894f6b0ed4572e21eb34026f1dc8869cb9ee3869029131bc8c3feb2d" +"checksum params 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5742da3da6ce04425a65fcd298d3ffbaae5597a8c9acb10203a986ee35ae3be8" +"checksum persistent 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b5830c40f2c2d6b4f14ed2221d16b72aa3224869e11e5f3434602fc2caeea876" +"checksum phf 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)" = "447d9d45f2e0b4a9b532e808365abf18fc211be6ca217202fcd45236ef12f026" +"checksum phf_codegen 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)" = "8af7ae7c3f75a502292b491e5cc0a1f69e3407744abe6e57e2a3b712bb82f01d" +"checksum phf_generator 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)" = "db005608fd99800c8c74106a7c894cf582055b689aa14a79462cefdcb7dc1cc3" +"checksum phf_shared 0.7.14 (registry+https://github.com/rust-lang/crates.io-index)" = "fee4d039930e4f45123c9b15976cf93a499847b6483dc09c42ea0ec4940f2aa6" +"checksum pkg-config 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8cee804ecc7eaf201a4a207241472cc870e825206f6c031e3ee2a72fa425f2fa" +"checksum plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1a6a0dc3910bc8db877ffed8e457763b317cf880df4ae19109b9f77d277cf6e0" +"checksum pnacl-build-helper 1.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "61c9231d31aea845007443d62fcbb58bb6949ab9c18081ee1e09920e0cf1118b" +"checksum postgres 0.11.8 (registry+https://github.com/rust-lang/crates.io-index)" = "43736d6b7fe17eadadbff10fb62d21955fe38c1b3f1db996c812852da3ec6755" +"checksum pulldown-cmark 0.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1058d7bb927ca067656537eec4e02c2b4b70eaaa129664c5b90c111e20326f41" +"checksum quick-error 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0aad603e8d7fb67da22dbdf1f4b826ce8829e406124109e73cf1b2454b93a71c" +"checksum r2d2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a63c7dd6655b3165145c5c140e8548ba2176a263682c07aaead2fe79eedd97bc" +"checksum r2d2_postgres 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6a89004a606289a10237da3c2676afcbf5ab23fedbab23b0f516cb9d9067ca" +"checksum rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "2791d88c6defac799c3f20d74f094ca33b9332612d9aef9078519c82e4fe04a5" +"checksum regex 0.1.71 (registry+https://github.com/rust-lang/crates.io-index)" = "e58a1b7d2bfecc0746e8587c30a53d01ea7bc0e98fac54e5aaa375b94338a0cc" +"checksum regex-syntax 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "baa04823ba7be7ed0bed3d0704c7b923019d9c4e4931c5af2804c7c7a0e3d00b" +"checksum route-recognizer 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "4f0a750d020adb1978f5964ea7bca830585899b09da7cbb3f04961fc2400122d" +"checksum router 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc0fcc2c2e4bc697c891411eab305bec011d5ea8c9b1b9f4805f1efa22378d3" +"checksum rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)" = "6159e4e6e559c81bd706afe9c8fd68f547d3e851ce12e76b1de7914bab61691b" +"checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084" +"checksum semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)" = "d4f410fedcf71af0345d7607d246e7ad15faaadd49d240ee3b24e5dc21a820ac" +"checksum semver 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2d5b7638a1f03815d94e88cb3b3c08e87f0db4d683ef499d1836aaf70a45623f" +"checksum sequence_trie 0.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "d5b4eb0f7d1ff9b9666d8b8ff543f3705dd464025269a5b0e1988ffa60ca1be8" +"checksum serde 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "781eed7bf0a59a4b489d761c4eaf9a364cdb036c0656a68cc9c0a761478a537e" +"checksum serde_json 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2c88a751caa8f0000058fb971cd443ed2e6b653f33f5a47f29892a8bd44ca4c1" +"checksum slug 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39af1ce888a1253c8b9fcfa36626557650fb487c013620a743262d2769a3e9f3" +"checksum solicit 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "172382bac9424588d7840732b250faeeef88942e37b6e35317dce98cafdd75b2" +"checksum staticfile 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c684ac0e2be10aa128647caaf539157c3bebc7a8a9693f0d97a42596d9753286" +"checksum strsim 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e4d73a2c36a4d095ed1a6df5cbeac159863173447f7a82b3f4757426844ab825" +"checksum strsim 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0d5f575d5ced6634a5c4cb842163dab907dc7e9148b28dc482d81b8855cbe985" +"checksum tar 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "59a5014d9493d6c2799dff5da5d780c2f159bf368c54f115dbc421d02f1d5496" +"checksum tempdir 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "0b62933a3f96cd559700662c34f8bab881d9e3540289fb4f368419c7f13a5aa9" +"checksum term 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3deff8a2b3b6607d6d7cc32ac25c0b33709453ca9cceac006caac51e963cf94a" +"checksum thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03" +"checksum thread_local 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "55dd963dbaeadc08aa7266bf7f91c3154a7805e32bb94b820b769d2ef3b4744d" +"checksum time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "3c7ec6d62a20df54e07ab3b78b9a3932972f4b7981de295563686849eb3989af" +"checksum toml 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)" = "0590d72182e50e879c4da3b11c6488dae18fccb1ae0c7a3eda18e16795844796" +"checksum traitobject 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "07eaeb7689bb7fca7ce15628319635758eda769fed481ecfe6686ddef2600616" +"checksum traitobject 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9dc23794ff47c95882da6f9d15de9a6be14987760a28cc0aafb40b7675ef09d8" +"checksum typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" +"checksum typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6" +"checksum unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "13a5906ca2b98c799f4b1ab4557b76367ebd6ae5ef14930ec841c74aed5f3764" +"checksum unicode-bidi 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c1f7ceb96afdfeedee42bade65a0d585a6a0106f681b6749c8ff4daa8df30b3f" +"checksum unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "26643a2f83bac55f1976fb716c10234485f9202dcd65cfbdf9da49867b271172" +"checksum unicode-width 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2d6722facc10989f63ee0e20a83cd4e1714a9ae11529403ac7e0afd069abc39e" +"checksum unidecode 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d2adb95ee07cd579ed18131f2d9e7a17c25a4b76022935c7f2460d2bfae89fd2" +"checksum unsafe-any 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b351086021ebc264aea3ab4f94d61d889d98e5e9ec2d985d993f50133537fd3a" +"checksum url 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)" = "cbaa8377a162d88e7d15db0cf110c8523453edcbc5bc66d2b6fffccffa34a068" +"checksum url 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)" = "f6d04073d0fcd045a1cf57aea560d1be5ba812d8f28814e1e1cf0e90ff4d2f03" +"checksum url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8ab4ca6f0107350f41a59a51cb0e71a04d905bc6a29181d2cb42fa4f040c65c9" +"checksum urlencoded 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dfb887e98fde9fed0a0c84ec113188d65214a17199a497f8f0fc3d973a6d1ad7" +"checksum user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ef4711d107b21b410a3a974b1204d9accc8b10dad75d8324b5d755de1617d47" +"checksum utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f" +"checksum uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "78c590b5bd79ed10aad8fb75f078a59d8db445af6c743e55c4a53227fc01c13f" +"checksum uuid 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a5efe91619a89528042b9b513ee41dc1ed06b35dc77b1e7a945a1caf580b863" +"checksum vec_map 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cac5efe5cb0fa14ec2f84f83c701c562ee63f6dcc680861b21d65c682adfb05f" +"checksum walkdir 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "7ad450634b9022aeb0e8e7f1c79c1ded92d0fc5bee831033d148479771bd218d" +"checksum winapi 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3969e500d618a5e974917ddefd0ba152e4bcaae5eb5d9b8c1fbc008e9e28c24e" +"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" +"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" diff --git a/Cargo.toml b/Cargo.toml index 53f6ccec8..56197d063 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ build = "build.rs" log = "0.3" rustc-serialize = "0.3" regex = "0.1" -postgres = { version = "0.11.7", features = [ "time", "rustc-serialize" ] } +postgres = { version = "^0.11", features = [ "time", "rustc-serialize" ] } clap = "2.5.2" time = "0.1" hyper = "0.9.7" @@ -20,6 +20,16 @@ slug = "^0.1.1" env_logger = "0.3" git2 = "0.4" magic = "^0.10.0" +iron = "0.3.0" +router = "0.1.1" +staticfile = { version = "0.2.0", features = [ "cache" ] } +handlebars-iron = "0.15.2" +pulldown-cmark = "0.0.8" +r2d2 = "0.7.0" +r2d2_postgres = "0.10.1" +url = "1.1.1" +params = "0.2.2" +libc = "0.2" [dependencies.cargo] git = "https://github.com/rust-lang/cargo.git" diff --git a/INSTALLATION.md b/INSTALLATION.md new file mode 100644 index 000000000..1f2016e2c --- /dev/null +++ b/INSTALLATION.md @@ -0,0 +1,172 @@ +# Installation Guide + +## Requirements + +`cratesfyi` is written in Rust and requires Rust compiler. It is developed under +Linux, it may not work or compile under other operating systems. List of +requirements to compile `cratesfyi`: + +* rustc compiler +* gcc +* cmake +* pkg-config +* openssl (development files) +* libmagic (development files) +* git +* postgresql +* libc (development files) +* lxc + +## `cratesfyi-prefix` directory + +`cratesfyi` is using a prefix directory for: + +* Clone of `crates.io-index` +* `documentations` directory to copy generated documentation +* `sources` directory +* `public_html` directory to serve some static files +* `cratesfyi-container` a Linux container (LXC) to build crates in a safe + environment + +An example script to create cratesfyi-prefix directory (note: you don't need +setup a lxc-container if you are gonna run only web interface, +skip to setting up database): + + +```sh +#!/bin/sh +# Creates cratesfyi-prefix directory for cratesfyi +# This script is designed to run on Debian based operating systems, +# and tested under Debian jessie and sid + +set -e + +PREFIX=$(pwd)/cratesfyi-prefix +DIST_TEMPLATE=debian +DIST_RELEASE=jessie +DIST_MIRROR=http://httpredir.debian.org/debian + +mkdir $PREFIX +mkdir -p $PREFIX/sources $PREFIX/documentations +git clone https://github.com/rust-lang/crates.io-index.git $PREFIX/crates.io-index + +# Create debian8 lxc container into cratesfyi-container directory +# Use your own distribution template and release name +sudo LANG=C MIRROR=$DIST_MIRROR \ + lxc-create -n cratesfyi-container -P $PREFIX \ + -t $DIST_TEMPLATE -- -r $DIST_RELEASE + +# Due to some bug in lxc-attach this container +# must have a symbolic link in /var/lib/lxc +sudo ln -s $PREFIX/cratesfyi-container /var/lib/lxc + +# Container directory must be accessible by current user +sudo chmod 755 $PREFIX/cratesfyi-container + +# Setup lxc network +echo 'USE_LXC_BRIDGE="true" +LXC_BRIDGE="lxcbr0" +LXC_ADDR="10.0.3.1" +LXC_NETMASK="255.255.255.0" +LXC_NETWORK="10.0.3.0/24" +LXC_DHCP_RANGE="10.0.3.2,10.0.3.254" +LXC_DHCP_MAX="253" +LXC_DHCP_CONFILE="" +LXC_DOMAIN=""' | sudo tee /etc/default/lxc-net + +# Start network interface +sudo service lxc-net restart + +# Setup network for container +sudo sed -i 's/lxc.network.type.*/lxc.network.type = veth\nlxc.network.link = lxcbr0/' \ + $PREFIX/cratesfyi-container/config + +# Start lxc container +sudo lxc-start -n cratesfyi-container + +# Add user accounts into container +# cratesfyi is using multiple user accounts to run cargo simultaneously +for user in $(whoami) cratesfyi updater; do + sudo lxc-attach -n cratesfyi-container -- \ + adduser --disabled-login --disabled-password --gecos "" $user +done + +# Install required packages for rust installation +sudo lxc-attach -n cratesfyi-container -- apt-get update +sudo lxc-attach -n cratesfyi-container -- apt-get install -y file git curl sudo ca-certificates + +# Install rust nightly into container +sudo lxc-attach -n cratesfyi-container -- \ + su - -c 'curl -sSf https://static.rust-lang.org/rustup.sh | sh -s -- --channel=nightly' +``` + + +The last step is to install cratesfyi into the guest machine +(or build in guest machine). If your host and guest +operating system is same simply build cratesfyi in release mode and copy into +`/usr/local/bin` directory of guest system: + +```sh +cargo build --release +cp target/release/cratesfyi CRATESFYI_PREFIX_DIR/rootfs/usr/local/bin/ +``` + +cratesfyi is only using `lxd-attach` command with sudo. Make sure your user +account can use this command without root password. Example `sudoers` entry: + +```text +cratesfyi ALL=(ALL) NOPASSWD: /usr/bin/lxc-attach +``` + +### Setting up database + +cratesfyi is using postgresql database to store crate and build +information. You need to set up database before using chroot builder. To do +this: + +```text +$ sudo su - postgres -c psql +# First create a user +postgres=# CREATE USER cratesfyi WITH PASSWORD 'password'; +postgres=# CREATE DATABASE cratesfyi OWNER cratesfyi; +postgres=# \q +# Initialize database with cratesfyi +$ CRATESFYI_DATABASE_URL=postgresql://cratesfyi:password@localhost cratesfyi database init +``` + +## Environment variables + +`cratesfyi` is using various environment variables to run. Those are: + +```sh +HOME=/srv/cratesfyi +PATH=/srv/cratesfyi/.cargo/bin:/usr/local/bin:/usr/bin:/bin +CRATESFYI_PREFIX=/srv/cratesfyi/cratesfyi-prefix +CRATESFYI_DATABASE_URL='postgresql://cratesfyi@localhost' +CRATESFYI_GITHUB_USERNAME='USERNAME' +CRATESFYI_GITHUB_ACCESSTOKEN='TOKEN' +``` + +You can also specify `RUST_LOG=cratesfyi` to see all log messages. It will only +show `INFO` or more leveled log messages without any `RUST_LOG` environment +variable. + +Example systemd service file for cratesfyi: + +```text +[Unit] +Description=Cratesfyi daemon +After=network.target postgresql.service + +[Service] +User=cratesfyi +Group=cratesfyi +Type=forking +PIDFile=/srv/cratesfyi/cratesfyi-prefix/cratesfyi.pid +EnvironmentFile=/srv/cratesfyi/env +ExecStart=/bin/sh -c '/srv/cratesfyi/cratesfyi daemon > /srv/cratesfyi/cratesfyi-prefix/cratesfyi.log 2>&1' +WorkingDirectory=/srv/cratesfyi + +[Install] +WantedBy=multi-user.target +``` diff --git a/README.md b/README.md index 25f4fa676..4c0f95637 100644 --- a/README.md +++ b/README.md @@ -1,139 +1,46 @@ - -# docs.rs +# Docs.rs [![Build Status](https://secure.travis-ci.org/onur/docs.rs.svg?branch=master)](https://travis-ci.org/onur/docs.rs) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/onur/docs.rs/master/LICENSE) -Documentation host for the Rust Programming Language crates. - - -## Installation - -cratesfyi needs `cratesfyi-prefix` directory and a postgresql server to run. -This directory must have: - -* Clone of `crates.io-index` repository. -* `sources` directory for crate sources. -* `cratesfyi-container` lxc container for building crates. This container - must use exact same operating system as host machine to avoid conflicts - (or you can build cratesfyi in guest system). -* `documentations` directory for crate documentations. - - -An example script to create cratesfyi-prefix directory. Make sure you have -`git` and `lxc` packages installed. **Run this script as a normal user**: - - -```sh -#!/bin/sh -# Creates cratesfyi-prefix directory for cratesfyi -# This script is designed to run on Debian based operating systems, -# and tested under Debian jessie and sid - -set -e - -PREFIX=$(pwd)/cratesfyi-prefix -DIST_TEMPLATE=debian -DIST_RELEASE=jessie -DIST_MIRROR=http://httpredir.debian.org/debian - -mkdir $PREFIX -mkdir -p $PREFIX/sources $PREFIX/documentations -git clone https://github.com/rust-lang/crates.io-index.git $PREFIX/crates.io-index - -# Create debian8 lxc container into cratesfyi-container directory -# Use your own distribution template and release name -sudo LANG=C MIRROR=$DIST_MIRROR \ - lxc-create -n cratesfyi-container -P $PREFIX \ - -t $DIST_TEMPLATE -- -r $DIST_RELEASE - -# Due to some bug in lxc-attach this container -# must have a symbolic link in /var/lib/lxc -sudo ln -s $PREFIX/cratesfyi-container /var/lib/lxc - -# Container directory must be accessible by current user -sudo chmod 755 $PREFIX/cratesfyi-container - -# Setup lxc network -echo 'USE_LXC_BRIDGE="true" -LXC_BRIDGE="lxcbr0" -LXC_ADDR="10.0.3.1" -LXC_NETMASK="255.255.255.0" -LXC_NETWORK="10.0.3.0/24" -LXC_DHCP_RANGE="10.0.3.2,10.0.3.254" -LXC_DHCP_MAX="253" -LXC_DHCP_CONFILE="" -LXC_DOMAIN=""' | sudo tee /etc/default/lxc-net - -# Start network interface -sudo service lxc-net restart - -# Setup network for container -sudo sed -i 's/lxc.network.type.*/lxc.network.type = veth\nlxc.network.link = lxcbr0/' \ - $PREFIX/cratesfyi-container/config - -# Start lxc container -sudo lxc-start -n cratesfyi-container - -# Add user accounts into container -# cratesfyi is using multiple user accounts to run cargo simultaneously -for user in $(whoami) cratesfyi updater; do - sudo lxc-attach -n cratesfyi-container -- \ - adduser --disabled-login --disabled-password --gecos "" $user -done - -# Install required packages for rust installation -sudo lxc-attach -n cratesfyi-container -- apt-get update -sudo lxc-attach -n cratesfyi-container -- apt-get install -y file git curl sudo ca-certificates - -# Install rust nightly into container -sudo lxc-attach -n cratesfyi-container -- \ - su - -c 'curl -sSf https://static.rust-lang.org/rustup.sh | sh -s -- --channel=nightly' -``` - - -The last step is to install cratesfyi into the guest machine -(or build in guest machine). If your host and guest -operating system is same simply build cratesfyi in release mode and copy into -`/usr/local/bin` directory of guest system: +Docs.rs (formerly cratesfyi) is an open source project to host documentation +of crates for the Rust Programming Language. -```sh -cargo build --release -cp target/release/cratesfyi CRATESFYI_PREFIX_DIR/rootfs/usr/local/bin/ -``` +Docs.rs automatically builds crates' documentation released on crates.io using +the nightly release of the Rust compiler. -cratesfyi is only using `lxd-attach` command with sudo. Make sure your user -account can use this command without root password. Example `sudoers` entry: +The README of a crate is taken from the readme field defined in Cargo.toml. +If a crate doesn't have this field, no README will be displayed. -```text -yourusername ALL=(ALL) NOPASSWD: /usr/sbin/chroot -``` +### Redirections +Docs.rs is using semver to parse URLs. You can use this feature to access +crates' documentation easily. Example of URL redirections for `clap` crate: -### Setting up database +| URL | Redirects to documentation of | +|------------------------------|------------------------------------------------| +| | Latest version of clap | +| | 2.* version | +| | 2.9.* version | +| | 2.9.3 version (you don't need = unlike semver) | -cratesfyi is using postgresql database to store crate and build -information. You need to set up database before using chroot builder. To do -this: +The crates.fyi domain will redirect to docs.rs, supporting all of the +redirects discussed above -```sh -$ sudo su - postgres -c psql -# First create a user -postgres=# CREATE USER cratesfyi WITH PASSWORD 'password'; -postgres=# CREATE DATABASE cratesfyi OWNER cratesfyi; -postgres=# \q -# Initialize database with cratesfyi -CRATESFYI_DATABASE_URL=postgresql://cratesfyi:password@localhost ./cratesfyi database init -``` +#### Contributors -Make sure to export `CRATESFYI_DATABASE_URL` environment variable before -using cratesfyi. +* [Onur Aslan](https://github.com/onur]) +* [Corey Farwell](https://github.com/frewsxcv) +* [Jon Gjengset](https://github.com/jonhoo) +* [Matthew Hall](https://github.com/mattyhall) +* [Guillaume Gomez](https://github.com/GuillaumeGomez) +* [Mark Simulacrum](https://github.com/Mark-Simulacrum) +#### Sponsors -## Environment variables +Hosting generously provided by: -cratesfyi is using few environment variables: +![Leaseweb](https://docs.rs/leaseweb.gif) -* `CRATESFYI_PREFIX` Prefix directory for cratesfyi -* `CRATESFYI_DATABASE_URL` Postgresql database URL -* `RUST_LOG` Set this to desired log level to get log messages +If you are interested in sponsoring Docs.rs, please don't hesitate to +contact us at TODO. diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 000000000..de740541c --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ + +format_strings = false diff --git a/src/bin/cratesfyi.rs b/src/bin/cratesfyi.rs index 6bffafeae..a0c38e886 100644 --- a/src/bin/cratesfyi.rs +++ b/src/bin/cratesfyi.rs @@ -5,6 +5,7 @@ extern crate clap; #[macro_use] extern crate log; extern crate env_logger; +extern crate time; use std::env; @@ -14,11 +15,12 @@ use std::path::PathBuf; use clap::{Arg, App, SubCommand}; use cratesfyi::{DocBuilder, DocBuilderOptions, db}; use cratesfyi::utils::build_doc; +use cratesfyi::start_web_server; use cratesfyi::db::add_path_into_database; pub fn main() { - let _ = env_logger::init(); + logger_init(); let matches = App::new("cratesfyi") .version(cratesfyi::BUILD_VERSION) @@ -90,6 +92,14 @@ pub fn main() { .help("Version of crate"))) .subcommand(SubCommand::with_name("add-essential-files") .about("Adds essential files for rustc"))) + .subcommand(SubCommand::with_name("start-web-server") + .about("Starts web server") + .arg(Arg::with_name("SOCKET_ADDR") + .index(1) + .required(false) + .help("Socket address to listen to"))) + .subcommand(SubCommand::with_name("daemon") + .about("Starts cratesfyi daemon")) .subcommand(SubCommand::with_name("database") .about("Database operations") .subcommand(SubCommand::with_name("init") @@ -107,7 +117,8 @@ pub fn main() { .arg(Arg::with_name("PREFIX") .index(2) .help("Prefix of files in \ - database")))) + database"))) + .subcommand(SubCommand::with_name("update-release-activity"))) .get_matches(); @@ -187,8 +198,30 @@ pub fn main() { matches.value_of("PREFIX").unwrap_or(""), matches.value_of("DIRECTORY").unwrap()) .unwrap(); + } else if let Some(_) = matches.subcommand_matches("update-release-activity") { + // FIXME: This is actually util command not database + cratesfyi::utils::update_release_activity().expect("Failed to update release activity"); } + } else if let Some(matches) = matches.subcommand_matches("start-web-server") { + start_web_server(matches.value_of("SOCKET_ADDR")); + } else if let Some(_) = matches.subcommand_matches("daemon") { + cratesfyi::utils::start_daemon(); } else { println!("{}", matches.usage()); } } + + + +fn logger_init() { + let format = |record: &log::LogRecord| { + format!("{} [{}] {}: {}", + time::now().strftime("%Y/%m/%d %H:%M:%S").unwrap(), + record.level(), record.target(), record.args()) + }; + + let mut builder = env_logger::LogBuilder::new(); + builder.format(format); + builder.parse(&env::var("RUST_LOG").unwrap_or("cratesfyi=info".to_owned())); + builder.init().unwrap(); +} diff --git a/src/db/mod.rs b/src/db/mod.rs index 72a44a70b..f78e3170c 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -7,6 +7,8 @@ pub use self::file::add_path_into_database; use postgres::{Connection, SslMode}; use postgres::error::{Error, ConnectError}; use std::env; +use r2d2; +use r2d2_postgres; mod add_package; mod file; @@ -21,6 +23,52 @@ pub fn connect_db() -> Result { } +pub fn create_pool() -> r2d2::Pool { + let db_url = env::var("CRATESFYI_DATABASE_URL") + .expect("CRATESFYI_DATABASE_URL environment variable is not exists"); + let config = r2d2::Config::default(); + let manager = r2d2_postgres::PostgresConnectionManager::new(&db_url[..], + r2d2_postgres::SslMode::None) + .expect("Failed to create PostgresConnectionManager"); + r2d2::Pool::new(config, manager).expect("Failed to create r2d2 pool") +} + + +/// Updates content column in crates table. +/// +/// This column will be used for searches and always contains `tsvector` of: +/// +/// * crate name (rank A-weight) +/// * latest release description (rank B-weight) +/// * latest release keywords (rank B-weight) +/// * latest release readme (rank C-weight) +/// * latest release root rustdoc (rank C-weight) +pub fn update_search_index(conn: &Connection) -> Result { + conn.execute(" + WITH doc as ( + SELECT DISTINCT ON(releases.crate_id) + releases.id, + releases.crate_id, + setweight(to_tsvector(crates.name), 'A') || + setweight(to_tsvector(coalesce(releases.description, '')), 'B') || + setweight(to_tsvector(coalesce(( + SELECT string_agg(value, ' ') + FROM json_array_elements_text(releases.keywords)), '')), 'B') || + setweight(to_tsvector(coalesce(releases.readme, '')), 'C') || + setweight(to_tsvector(coalesce(releases.description_long, '')), 'C') as content + FROM releases + INNER JOIN crates ON crates.id = releases.crate_id + ORDER BY releases.crate_id, releases.release_time DESC + ) + UPDATE crates + SET latest_version_id = doc.id, + content = doc.content + FROM doc + WHERE crates.id = doc.crate_id AND + (crates.latest_version_id = 0 OR crates.latest_version_id != doc.id);", &[]) +} + + /// Creates database tables pub fn create_tables(conn: &Connection) -> Result<(), Error> { let queries = [ @@ -35,7 +83,8 @@ pub fn create_tables(conn: &Connection) -> Result<(), Error> { github_forks INT DEFAULT 0, \ github_issues INT DEFAULT 0, \ github_last_commit TIMESTAMP, \ - github_last_update TIMESTAMP \ + github_last_update TIMESTAMP, \ + content tsvector \ )", "CREATE TABLE releases ( \ id SERIAL PRIMARY KEY, \ @@ -119,6 +168,7 @@ pub fn create_tables(conn: &Connection) -> Result<(), Error> { date_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, \ content BYTEA \ )", + "CREATE INDEX content_idx ON crates USING gin(content)", "CREATE TABLE config ( \ name VARCHAR(100) NOT NULL PRIMARY KEY, \ value JSON NOT NULL \ diff --git a/src/docbuilder/options.rs b/src/docbuilder/options.rs index 8adf44c8a..020f45f95 100644 --- a/src/docbuilder/options.rs +++ b/src/docbuilder/options.rs @@ -4,6 +4,7 @@ use std::{env, fmt}; use std::path::PathBuf; +#[derive(Clone)] pub struct DocBuilderOptions { pub keep_build_directory: bool, pub prefix: PathBuf, diff --git a/src/docbuilder/queue.rs b/src/docbuilder/queue.rs index b2ea8fde0..24c246e88 100644 --- a/src/docbuilder/queue.rs +++ b/src/docbuilder/queue.rs @@ -116,7 +116,7 @@ impl DocBuilder { match self.build_package(&name[..], &version[..]) { Ok(_) => { let _ = conn.execute("DELETE FROM queue WHERE id = $1", &[&id]); - } + }, Err(e) => { error!("Failed to build package {}-{} from queue: {}", name, diff --git a/src/lib.rs b/src/lib.rs index 0d972fbe6..327fee5c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +//! [Docs.rs](https://docs.rs) (formerly cratesfyi) is an open source project to host +//! documentation of crates for the Rust Programming Language. #[macro_use] extern crate log; @@ -11,15 +13,27 @@ extern crate semver; extern crate slug; extern crate git2; extern crate magic; +extern crate iron; +extern crate router; +extern crate staticfile; +extern crate handlebars_iron; +extern crate pulldown_cmark; +extern crate r2d2; +extern crate r2d2_postgres; +extern crate url; +extern crate params; +extern crate libc; pub use self::docbuilder::DocBuilder; pub use self::docbuilder::ChrootBuilderResult; pub use self::docbuilder::error::DocBuilderError; pub use self::docbuilder::options::DocBuilderOptions; +pub use self::web::start_web_server; pub mod db; pub mod utils; mod docbuilder; +mod web; /// Version string generated at build time contains last git diff --git a/src/utils/daemon.rs b/src/utils/daemon.rs new file mode 100644 index 000000000..a8d567a00 --- /dev/null +++ b/src/utils/daemon.rs @@ -0,0 +1,150 @@ +//! Simple daemon +//! +//! This daemon will start web server, track new packages and build them + + +use std::{env, thread}; +use std::process::exit; +use std::fs::File; +use std::io::Write; +use std::time::Duration; +use std::path::PathBuf; +use libc::fork; +use time; +use DocBuilderOptions; +use DocBuilder; +use utils::{update_sources, update_release_activity, github_updater}; +use db::{connect_db, update_search_index}; + + + +pub fn start_daemon() { + // first check required environment variables + for v in ["CRATESFYI_PREFIX", + "CRATESFYI_PREFIX", + "CRATESFYI_GITHUB_USERNAME", + "CRATESFYI_GITHUB_ACCESSTOKEN"] + .iter() { + env::var(v).expect("Environment variable not found"); + } + + let dbopts = opts(); + + // check paths once + dbopts.check_paths().unwrap(); + + // fork the process + let pid = unsafe { fork() }; + if pid > 0 { + let mut file = File::create(dbopts.prefix.join("cratesfyi.pid")) + .expect("Failed to create pid file"); + writeln!(&mut file, "{}", pid).expect("Failed to write pid"); + + info!("cratesfyi {} daemon started on: {}", ::BUILD_VERSION, pid); + exit(0); + } + + + // check new crates every 15 minutes + // FIXME: why not just check new crates and build them if there is any? + thread::spawn(move || { + loop { + thread::sleep(Duration::from_secs(900)); + debug!("Checking new crates"); + let doc_builder = DocBuilder::new(opts()); + if let Err(e) = doc_builder.get_new_crates() { + error!("Failed to get new crates: {}", e); + } + } + }); + + + // build crates in que every 12 minutes + thread::spawn(move || { + loop { + thread::sleep(Duration::from_secs(720)); + + let mut opts = opts(); + opts.skip_if_exists = true; + + // check lock file + if opts.prefix.join("cratesfyi.lock").exists() { + warn!("Lock file exits, skipping building new crates"); + continue; + } + + // update index + if let Err(e) = update_sources() { + error!("Failed to update sources: {}", e); + continue; + } + + let mut doc_builder = DocBuilder::new(opts); + if let Err(e) = doc_builder.load_cache() { + error!("Failed to load cache: {}", e); + continue; + } + + debug!("Building new crates"); + if let Err(e) = doc_builder.build_packages_queue() { + error!("Failed build new crates: {}", e); + } + + if let Err(e) = doc_builder.save_cache() { + error!("Failed to save cache: {}", e); + } + } + }); + + + // update release activity everyday at 02:00 + thread::spawn(move || { + loop { + thread::sleep(Duration::from_secs(60)); + let now = time::now(); + if now.tm_hour == 2 && now.tm_min == 0 { + info!("Updating release activity"); + if let Err(e) = update_release_activity() { + error!("Failed to update release activity: {}", e); + } + } + } + }); + + + // update search index every 3 hours + thread::spawn(move || { + loop { + thread::sleep(Duration::from_secs(60 * 60 * 3)); + let conn = connect_db().expect("Failed to connect database"); + if let Err(e) = update_search_index(&conn) { + error!("Failed to update search index: {}", e); + } + } + }); + + + // update github stats every 6 hours + thread::spawn(move || { + loop { + thread::sleep(Duration::from_secs(60 * 60 * 6)); + if let Err(e) = github_updater() { + error!("Failed to update github fields: {}", e); + } + } + }); + + // TODO: update ssl certificate every 3 months + + // at least start web server + info!("Starting web server"); + ::start_web_server(None); +} + + + +fn opts() -> DocBuilderOptions { + let prefix = PathBuf::from(env::var("CRATESFYI_PREFIX") + .expect("CRATESFYI_PREFIX environment variable not found")); + DocBuilderOptions::from_prefix(prefix) +} diff --git a/src/utils/github_updater.rs b/src/utils/github_updater.rs index 52faac909..307ee2afa 100644 --- a/src/utils/github_updater.rs +++ b/src/utils/github_updater.rs @@ -54,7 +54,7 @@ pub fn github_updater() -> Result<(), DocBuilderError> { &crate_id]) .map_err(DocBuilderError::DatabaseError) }) { - error!("Failed to update github fields of: {} {}", crate_name, err); + debug!("Failed to update github fields of: {} {}", crate_name, err); } // sleep for rate limits diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 34c854be2..47c87f3bc 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,7 +4,11 @@ pub use self::build_doc::{build_doc, get_package, source_path, update_sources}; pub use self::copy::{copy_dir, copy_doc_dir}; pub use self::github_updater::github_updater; +pub use self::release_activity_updater::update_release_activity; +pub use self::daemon::start_daemon; mod github_updater; mod build_doc; mod copy; +mod release_activity_updater; +mod daemon; diff --git a/src/utils/release_activity_updater.rs b/src/utils/release_activity_updater.rs new file mode 100644 index 000000000..64b26c8ae --- /dev/null +++ b/src/utils/release_activity_updater.rs @@ -0,0 +1,63 @@ + +use db::connect_db; +use DocBuilderError; +use time::{now, Duration}; +use std::collections::BTreeMap; +use rustc_serialize::json::ToJson; + + +pub fn update_release_activity() -> Result<(), DocBuilderError> { + + let conn = try!(connect_db()); + let mut dates = Vec::new(); + let mut crate_counts = Vec::new(); + + for day in 1..31 { + let rows = try!(conn.query(&format!("SELECT COUNT(*) + FROM releases + WHERE release_time < NOW() - INTERVAL '{} day' AND + release_time > NOW() - INTERVAL '{} day'", + day, + day + 1), + &[])); + let release_count: i64 = rows.get(0).get(0); + let now = now(); + let date = now - Duration::days(day); + dates.push(format!("{}", date.strftime("%d %b").unwrap())); + // unwrap is fine here, ~~~~~~~~~~~~^ our date format is always valid + crate_counts.push(release_count); + } + + dates.reverse(); + crate_counts.reverse(); + + let map = { + let mut map = BTreeMap::new(); + map.insert("dates".to_owned(), dates.to_json()); + map.insert("counts".to_owned(), crate_counts.to_json()); + map.to_json() + }; + + try!(conn.query("INSERT INTO config (name, value) VALUES ('release_activity', $1)", + &[&map]) + .or_else(|_| { + conn.query("UPDATE config SET value = $1 WHERE name = 'release_activity'", + &[&map]) + })); + + Ok(()) +} + + +#[cfg(test)] +mod test { + extern crate env_logger; + use super::update_release_activity; + + #[test] + #[ignore] + fn test_update_release_activity() { + let _ = env_logger::init(); + assert!(update_release_activity().is_ok()); + } +} diff --git a/src/web/builds.rs b/src/web/builds.rs new file mode 100644 index 000000000..1c017f91c --- /dev/null +++ b/src/web/builds.rs @@ -0,0 +1,124 @@ + + +use std::collections::BTreeMap; +use super::MetaData; +use super::pool::Pool; +use super::duration_to_str; +use super::page::Page; +use iron::prelude::*; +use time; +use router::Router; +use rustc_serialize::json::{Json, ToJson}; + + + +#[derive(Clone)] +struct Build { + id: i32, + rustc_version: String, + cratesfyi_version: String, + build_status: bool, + build_time: time::Timespec, + output: Option, +} + + +struct BuildsPage { + metadata: Option, + builds: Vec, + build_details: Option, +} + + +impl ToJson for Build { + fn to_json(&self) -> Json { + let mut m: BTreeMap = BTreeMap::new(); + m.insert("id".to_owned(), self.id.to_json()); + m.insert("rustc_version".to_owned(), self.rustc_version.to_json()); + m.insert("cratesfyi_version".to_owned(), + self.cratesfyi_version.to_json()); + m.insert("build_status".to_owned(), self.build_status.to_json()); + m.insert("build_time".to_owned(), + duration_to_str(self.build_time).to_json()); + m.insert("output".to_owned(), self.output.to_json()); + m.to_json() + } +} + + +impl ToJson for BuildsPage { + fn to_json(&self) -> Json { + let mut m: BTreeMap = BTreeMap::new(); + m.insert("metadata".to_owned(), self.metadata.to_json()); + m.insert("builds".to_owned(), self.builds.to_json()); + m.insert("build_details".to_owned(), self.build_details.to_json()); + m.to_json() + } +} + + +pub fn build_list_handler(req: &mut Request) -> IronResult { + + let name = req.extensions.get::().unwrap().find("name").unwrap(); + let version = req.extensions.get::().unwrap().find("version").unwrap(); + let req_build_id: i32 = req.extensions + .get::() + .unwrap() + .find("id") + .unwrap_or("0") + .parse() + .unwrap_or(0); + + let conn = req.extensions.get::().unwrap(); + + let mut build_list: Vec = Vec::new(); + let mut build_details = None; + + // FIXME: getting builds.output may cause performance issues when release have tons of builds + for row in &conn.query("SELECT crates.name, \ + releases.version, \ + releases.description, \ + releases.rustdoc_status, \ + releases.target_name, \ + builds.id, \ + builds.rustc_version, \ + builds.cratesfyi_version, \ + builds.build_status, \ + builds.build_time, \ + builds.output \ + FROM builds \ + INNER JOIN releases ON releases.id = builds.rid \ + INNER JOIN crates ON releases.crate_id = crates.id \ + WHERE crates.name = $1 AND releases.version = $2", + &[&name, &version]) + .unwrap() { + + let id: i32 = row.get(5); + + let build = Build { + id: id, + rustc_version: row.get(6), + cratesfyi_version: row.get(7), + build_status: row.get(8), + build_time: row.get(9), + output: row.get(10), + }; + + if id == req_build_id { + build_details = Some(build.clone()); + } + + build_list.push(build); + } + + let builds_page = BuildsPage { + metadata: MetaData::from_crate(&conn, &name, &version), + builds: build_list, + build_details: build_details, + }; + + Page::new(builds_page) + .set_true("show_package_navigation") + .set_true("package_navigation_builds_tab") + .to_resp("builds") +} diff --git a/src/web/crate_details.rs b/src/web/crate_details.rs new file mode 100644 index 000000000..2cc94f83e --- /dev/null +++ b/src/web/crate_details.rs @@ -0,0 +1,210 @@ + + + +use super::{MetaData, duration_to_str, match_version, render_markdown}; +use super::error::Nope; +use super::page::Page; +use db::connect_db; +use iron::prelude::*; +use iron::status; +use std::collections::BTreeMap; +use time; +use rustc_serialize::json::{Json, ToJson}; +use router::Router; +use postgres::Connection; +use semver; + + +// TODO: Add target name and versions + + +#[derive(Debug)] +struct CrateDetails { + name: String, + version: String, + description: Option, + authors: Vec<(String, String)>, + authors_json: Option, + dependencies: Option, + readme: Option, + rustdoc: Option, // this is description_long in database + release_time: time::Timespec, + build_status: bool, + rustdoc_status: bool, + repository_url: Option, + homepage_url: Option, + keywords: Option, + have_examples: bool, // need to check this manually + target_name: Option, + versions: Vec, + github: bool, // is crate hosted in github + github_stars: Option, + github_forks: Option, + github_issues: Option, + metadata: MetaData, + is_library: bool, +} + + +impl ToJson for CrateDetails { + fn to_json(&self) -> Json { + let mut m: BTreeMap = BTreeMap::new(); + m.insert("name".to_string(), self.name.to_json()); + m.insert("version".to_string(), self.version.to_json()); + m.insert("description".to_string(), self.description.to_json()); + m.insert("authors".to_string(), self.authors.to_json()); + m.insert("authors_json".to_string(), self.authors_json.to_json()); + m.insert("dependencies".to_string(), self.dependencies.to_json()); + if let Some(ref readme) = self.readme { + m.insert("readme".to_string(), render_markdown(&readme).to_json()); + } + if let Some(ref rustdoc) = self.rustdoc { + m.insert("rustdoc".to_string(), render_markdown(&rustdoc).to_json()); + } + m.insert("release_time".to_string(), + duration_to_str(self.release_time).to_json()); + m.insert("build_status".to_string(), self.build_status.to_json()); + m.insert("rustdoc_status".to_string(), self.rustdoc_status.to_json()); + m.insert("repository_url".to_string(), self.repository_url.to_json()); + m.insert("homepage_url".to_string(), self.homepage_url.to_json()); + m.insert("keywords".to_string(), self.keywords.to_json()); + m.insert("have_examples".to_string(), self.have_examples.to_json()); + m.insert("target_name".to_string(), self.target_name.to_json()); + m.insert("versions".to_string(), self.versions.to_json()); + m.insert("github".to_string(), self.github.to_json()); + m.insert("github_stars".to_string(), self.github_stars.to_json()); + m.insert("github_forks".to_string(), self.github_forks.to_json()); + m.insert("github_issues".to_string(), self.github_issues.to_json()); + m.insert("metadata".to_string(), self.metadata.to_json()); + m.insert("is_library".to_string(), self.is_library.to_json()); + m.to_json() + } +} + + +impl CrateDetails { + fn new(conn: &Connection, name: &str, version: &str) -> Option { + + // get all stuff, I love you rustfmt + let query = "SELECT crates.name, \ + releases.version, \ + releases.description, \ + releases.authors, \ + releases.dependencies, \ + releases.readme, \ + releases.description_long, \ + releases.release_time, \ + releases.build_status, \ + releases.rustdoc_status, \ + releases.repository_url, \ + releases.homepage_url, \ + releases.keywords, \ + releases.have_examples, \ + releases.target_name, \ + crates.versions, \ + authors.name, \ + authors.slug, \ + crates.github_stars, \ + crates.github_forks, \ + crates.github_issues, \ + releases.is_library \ + FROM author_rels \ + LEFT OUTER JOIN authors ON authors.id = author_rels.aid \ + LEFT OUTER JOIN releases ON releases.id = author_rels.rid \ + LEFT OUTER JOIN crates ON crates.id = releases.crate_id \ + WHERE crates.name = $1 AND releases.version = $2;"; + + let rows = conn.query(query, &[&name, &version]).unwrap(); + + if rows.len() == 0 { + return None; + } + + // sort versions with semver + let versions = { + let mut versions: Vec = Vec::new(); + let versions_from_db: Json = rows.get(0).get(15); + + versions_from_db.as_array().map(|vers| { + for version in vers { + version.as_string().map(|version| { + if let Ok(sem_ver) = semver::Version::parse(&version) { + versions.push(sem_ver); + }; + }); + } + }); + + versions.sort(); + versions.reverse(); + versions.iter().map(|semver| format!("{}", semver)).collect() + }; + + let metadata = MetaData { + name: rows.get(0).get(0), + version: rows.get(0).get(1), + description: rows.get(0).get(2), + rustdoc_status: rows.get(0).get(9), + target_name: rows.get(0).get(14), + }; + + let mut crate_details = CrateDetails { + name: rows.get(0).get(0), + version: rows.get(0).get(1), + description: rows.get(0).get(2), + authors: Vec::new(), + authors_json: rows.get(0).get(3), + dependencies: rows.get(0).get(4), + readme: rows.get(0).get(5), + rustdoc: rows.get(0).get(6), + release_time: rows.get(0).get(7), + build_status: rows.get(0).get(8), + rustdoc_status: rows.get(0).get(9), + repository_url: rows.get(0).get(10), + homepage_url: rows.get(0).get(11), + keywords: rows.get(0).get(12), + have_examples: rows.get(0).get(13), + target_name: rows.get(0).get(14), + versions: versions, + github: false, + github_stars: rows.get(0).get(18), + github_forks: rows.get(0).get(19), + github_issues: rows.get(0).get(20), + metadata: metadata, + is_library: rows.get(0).get(21), + }; + + if let Some(repository_url) = crate_details.repository_url.clone() { + crate_details.github = repository_url.starts_with("http://github.com") || + repository_url.starts_with("https://github.com"); + } + + // Insert authors with name and slug + for row in &rows { + crate_details.authors.push((row.get(16), row.get(17))); + } + + Some(crate_details) + } +} + + + +pub fn crate_details_handler(req: &mut Request) -> IronResult { + // this handler must always called with a crate name + let name = req.extensions.get::().unwrap().find("name").unwrap(); + let req_version = req.extensions.get::().unwrap().find("version"); + + let conn = connect_db().unwrap(); + + match_version(&conn, &name, req_version) + .and_then(|version| CrateDetails::new(&conn, &name, &version)) + .ok_or(IronError::new(Nope::CrateNotFound, status::NotFound)) + .and_then(|details| { + Page::new(details) + .set_true("show_package_navigation") + .set_true("javascript_highlightjs") + .set_true("package_navigation_crate_tab") + .to_resp("crate_details") + }) +} diff --git a/src/web/error.rs b/src/web/error.rs new file mode 100644 index 000000000..f7ed1857b --- /dev/null +++ b/src/web/error.rs @@ -0,0 +1,68 @@ +use std::error::Error; +use iron::prelude::*; +use iron::Handler; +use iron::status; +use web::page::Page; +use std::fmt; + +#[derive(Debug, Copy, Clone)] +pub enum Nope { + ResourceNotFound, + CrateNotFound, + NoResults, +} + +impl fmt::Display for Nope { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.description()) + } +} + +impl Error for Nope { + fn description(&self) -> &str { + match *self { + Nope::ResourceNotFound => "Requested resource not found", + Nope::CrateNotFound => "Requested crate not found", + Nope::NoResults => "Search yielded no results", + } + } +} + +impl Handler for Nope { + fn handle(&self, req: &mut Request) -> IronResult { + match *self { + Nope::ResourceNotFound => { + // user tried to navigate to a resource (doc page/file) that doesn't exist + Page::new("no such resource".to_owned()) + .set_status(status::NotFound) + .title("The requested resource does not exist") + .to_resp("error") + } + Nope::CrateNotFound => { + // user tried to navigate to a crate that doesn't exist + Page::new("no such crate".to_owned()) + .set_status(status::NotFound) + .title("The requested crate does not exist") + .to_resp("error") + } + Nope::NoResults => { + use params::{Params, Value}; + let params = req.get::().unwrap(); + if let Some(&Value::String(ref query)) = params.find(&["query"]) { + // this used to be a search + Page::new(Vec::::new()) + .set_status(status::NotFound) + .set("search_query", &query) + .title(&format!("No crates found matching '{}'", query)) + .to_resp("releases") + } else { + // user did a search with no search terms + Page::new(Vec::::new()) + .set_status(status::NotFound) + .title("No results given for empty search query") + .to_resp("releases") + } + } + } + } +} diff --git a/src/web/file.rs b/src/web/file.rs new file mode 100644 index 000000000..a67a3ce3a --- /dev/null +++ b/src/web/file.rs @@ -0,0 +1,93 @@ +//! Database based file handler + +use super::pool::Pool; +use time; +use postgres::Connection; +use iron::{Handler, Request, IronResult, Response, IronError}; +use iron::status; + + +pub struct File { + pub path: String, + pub mime: String, + pub date_added: time::Timespec, + pub date_updated: time::Timespec, + pub content: Vec, +} + + +impl File { + /// Gets file from database + pub fn from_path(conn: &Connection, path: &str) -> Option { + + let rows = conn.query("SELECT path, mime, date_added, date_updated, content FROM files \ + WHERE path = $1", + &[&path]) + .unwrap(); + + if rows.len() == 0 { + None + } else { + let row = rows.get(0); + Some(File { + path: row.get(0), + mime: row.get(1), + date_added: row.get(2), + date_updated: row.get(3), + content: row.get(4), + }) + } + } + + + /// Consumes File and creates a iron response + pub fn serve(self) -> Response { + use iron::headers::{CacheControl, LastModified, CacheDirective, HttpDate, ContentType}; + + let mut response = Response::with((status::Ok, self.content)); + let cache = vec![CacheDirective::Public, + CacheDirective::MaxAge(super::STATIC_FILE_CACHE_DURATION as u32)]; + response.headers.set(ContentType(self.mime.parse().unwrap())); + response.headers.set(CacheControl(cache)); + response.headers.set(LastModified(HttpDate(time::at(self.date_updated)))); + response + } + + + /// Checks if mime type of file is "application/x-empty" + pub fn is_empty(&self) -> bool { + self.mime == "application/x-empty" + } +} + + +/// Database based file handler for iron +/// +/// This is similar to staticfile crate, but its using getting files from database. +pub struct DatabaseFileHandler; + +impl Handler for DatabaseFileHandler { + fn handle(&self, req: &mut Request) -> IronResult { + + let path = { + let mut path = req.url.path.clone().join("/"); + if path.ends_with("/") { + path.push_str("index.html"); + } + // rustdoc javascripts have rustdoc prefix in database + // FIXME: this is kinda lame I have to save all javascripts with rustdoc prefix + if path.ends_with(".js") { + format!("rustdoc/{}", path) + } else { + path + } + }; + + let conn = req.extensions.get::().unwrap(); + if let Some(file) = File::from_path(&conn, &path) { + Ok(file.serve()) + } else { + Err(IronError::new(super::error::Nope::CrateNotFound, status::NotFound)) + } + } +} diff --git a/src/web/mod.rs b/src/web/mod.rs new file mode 100644 index 000000000..71b750c6f --- /dev/null +++ b/src/web/mod.rs @@ -0,0 +1,317 @@ +//! Web interface of cratesfyi + + +mod rustdoc; +mod releases; +mod page; +mod crate_details; +mod source; +mod pool; +mod file; +mod builds; +mod error; + +use std::env; +use std::error::Error; +use std::time::Duration; +use std::path::PathBuf; +use iron::prelude::*; +use iron::Handler; +use router::{Router, NoRoute}; +use staticfile::Static; +use handlebars_iron::{HandlebarsEngine, DirectorySource}; +use time; +use postgres::Connection; +use semver::{Version, VersionReq}; +use rustc_serialize::json::{Json, ToJson}; +use std::collections::BTreeMap; + + + +/// Duration of static files for staticfile and DatabaseFileHandler (in seconds) +const STATIC_FILE_CACHE_DURATION: u64 = 60 * 60 * 24 * 30 * 12; // 12 months + + +struct CratesfyiHandler { + router_handler: Box, + database_file_handler: Box, + static_handler: Box, +} + + +impl CratesfyiHandler { + fn chain(base: H) -> Chain { + // TODO: Use DocBuilderOptions for paths + let mut hbse = HandlebarsEngine::new(); + hbse.add(Box::new(DirectorySource::new("./templates", ".hbs"))); + + // load templates + if let Err(e) = hbse.reload() { + panic!("Failed to load handlebar templates: {}", e.description()); + } + + let mut chain = Chain::new(base); + chain.link_before(pool::Pool::new()); + chain.link_after(hbse); + chain + } + + pub fn new() -> CratesfyiHandler { + let mut router = Router::new(); + router.get("/", releases::home_page); + router.get("/about", |_: &mut Request| { + page::Page::new(false).title("About Docs.rs").to_resp("about") + }); + router.get("/releases", releases::releases_handler); + router.get("/releases/recent/:page", releases::releases_handler); + router.get("/releases/stars", releases::stars_handler); + router.get("/releases/stars/:page", releases::stars_handler); + router.get("/releases/:author", releases::author_handler); + router.get("/releases/:author/:page", releases::author_handler); + router.get("/releases/activity", releases::activity_handler); + router.get("/releases/search", releases::search_handler); + router.get("/crate/:name", crate_details::crate_details_handler); + router.get("/crate/:name/", crate_details::crate_details_handler); + router.get("/crate/:name/:version", crate_details::crate_details_handler); + router.get("/crate/:name/:version/", crate_details::crate_details_handler); + router.get("/crate/:name/:version/builds", builds::build_list_handler); + router.get("/crate/:name/:version/builds/:id", builds::build_list_handler); + router.get("/crate/:name/:version/source/", source::source_browser_handler); + router.get("/crate/:name/:version/source/*", source::source_browser_handler); + router.get("/:crate", rustdoc::rustdoc_redirector_handler); + router.get("/:crate/", rustdoc::rustdoc_redirector_handler); + router.get("/:crate/:version", rustdoc::rustdoc_redirector_handler); + router.get("/:crate/:version/search-index.js", rustdoc::rustdoc_html_server_handler); + router.get("/:crate/:version/:target", rustdoc::rustdoc_redirector_handler); + router.get("/:crate/:version/:target/", rustdoc::rustdoc_html_server_handler); + router.get("/:crate/:version/:target/*.html", rustdoc::rustdoc_html_server_handler); + + let router_chain = Self::chain(router); + let prefix = PathBuf::from(env::var("CRATESFYI_PREFIX").unwrap()).join("public_html"); + let static_handler = Static::new(prefix) + .cache(Duration::from_secs(STATIC_FILE_CACHE_DURATION)); + + CratesfyiHandler { + router_handler: Box::new(router_chain), + database_file_handler: Box::new(file::DatabaseFileHandler), + static_handler: Box::new(static_handler), + } + } +} + + +impl Handler for CratesfyiHandler { + fn handle(&self, req: &mut Request) -> IronResult { + // try router first then db/static file handler + // return 404 if none of them return Ok + self.router_handler + .handle(req) + .or_else(|e| { + // if router fails try to serve files from database first + self.database_file_handler.handle(req).or(Err(e)) + }) + .or_else(|e| { + // and then try static handler. if all of them fails, return 404 + self.static_handler.handle(req).or(Err(e)) + }) + .or_else(|e| { + debug!("{}", e.description()); + let err = if let Some(err) = e.error.downcast::() { + *err + } else if e.error.downcast::().is_some() { + error::Nope::ResourceNotFound + } else { + panic!("all cratesfyi errors should be of type Nope"); + }; + Self::chain(err).handle(req) + }) + } +} + + + +fn match_version(conn: &Connection, name: &str, version: Option<&str>) -> Option { + + // version is an Option<&str> from router::Router::get + // need to decode first + use url::percent_encoding::percent_decode; + let req_version = version.and_then(|v| { + match percent_decode(v.as_bytes()).decode_utf8() { + Ok(p) => Some(p.into_owned()), + Err(_) => None, + } + }).unwrap_or("*".to_string()); + + let versions = { + + let mut versions = Vec::new(); + // get every version of a crate + for row in &conn.query("SELECT version \ + FROM releases \ + INNER JOIN crates ON crates.id = releases.crate_id \ + WHERE crates.name = $1", + &[&name]) + .unwrap() { + let version: String = row.get(0); + versions.push(version); + } + + // FIXME: Need to sort versions with semver, database is not keeping them sorted + versions + }; + + // first check for exact match + // we can't expect users to use semver in query + for version in &versions { + if version == &req_version { + return Some(version.clone()) + } + } + + // Now try to match with semver + let req_sem_ver = VersionReq::parse(&req_version).unwrap(); + + // we need to sort versions first + let versions_sem = { + let mut versions_sem: Vec = Vec::new(); + + for version in &versions { + versions_sem.push(Version::parse(&version).unwrap()); + } + + versions_sem.sort(); + versions_sem.reverse(); + versions_sem + }; + + for version in &versions_sem { + if req_sem_ver.matches(&version) { + return Some(format!("{}", version)) + } + } + + None +} + + + + + +/// Wrapper around the pulldown-cmark parser and renderer to render markdown +fn render_markdown(text: &str) -> String { + // I got this from mdBook::src::utils + use pulldown_cmark::{Parser, html, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES}; + let mut s = String::with_capacity(text.len() * 3 / 2); + + let mut opts = Options::empty(); + opts.insert(OPTION_ENABLE_TABLES); + opts.insert(OPTION_ENABLE_FOOTNOTES); + + let p = Parser::new_ext(&text, opts); + html::push_html(&mut s, p); + s +} + + + +/// Starts cratesfyi web server +pub fn start_web_server(sock_addr: Option<&str>) { + let cratesfyi = CratesfyiHandler::new(); + Iron::new(cratesfyi).http(sock_addr.unwrap_or("localhost:3000")).unwrap(); +} + + + +/// Converts Timespec to nice readable relative time string +fn duration_to_str(ts: time::Timespec) -> String { + + let tm = time::at(ts); + let delta = time::now() - tm; + + if delta.num_days() > 5 { + format!("{}", tm.strftime("%b %d, %Y").unwrap()) + } else if delta.num_days() > 1 { + format!("{} days ago", delta.num_days()) + } else if delta.num_days() == 1 { + "one day ago".to_string() + } else if delta.num_hours() > 1 { + format!("{} hours ago", delta.num_hours()) + } else if delta.num_hours() == 1 { + "an hour ago".to_string() + } else if delta.num_minutes() > 1 { + format!("{} minutes ago", delta.num_minutes()) + } else if delta.num_minutes() == 1 { + "one minute ago".to_string() + } else if delta.num_seconds() > 0 { + format!("{} seconds ago", delta.num_seconds()) + } else { + "just now".to_string() + } + +} + + + +/// MetaData used in header +#[derive(Debug)] +pub struct MetaData { + pub name: String, + pub version: String, + pub description: Option, + pub target_name: Option, + pub rustdoc_status: bool, +} + + +impl MetaData { + pub fn from_crate(conn: &Connection, name: &str, version: &str) -> Option { + for row in &conn.query("SELECT crates.name, + releases.version, + releases.description, + releases.target_name, + releases.rustdoc_status + FROM releases + INNER JOIN crates ON crates.id = releases.crate_id + WHERE crates.name = $1 AND releases.version = $2", + &[&name, &version]).unwrap() { + + return Some(MetaData { + name: row.get(0), + version: row.get(1), + description: row.get(2), + target_name: row.get(3), + rustdoc_status: row.get(4), + }); + } + + None + } +} + + +impl ToJson for MetaData { + fn to_json(&self) -> Json { + let mut m: BTreeMap = BTreeMap::new(); + m.insert("name".to_owned(), self.name.to_json()); + m.insert("version".to_owned(), self.version.to_json()); + m.insert("description".to_owned(), self.description.to_json()); + m.insert("target_name".to_owned(), self.target_name.to_json()); + m.insert("rustdoc_status".to_owned(), self.rustdoc_status.to_json()); + m.to_json() + } +} + + +#[cfg(test)] +mod test { + extern crate env_logger; + use super::*; + + #[test] + #[ignore] + fn test_start_web_server() { + // FIXME: This test is doing nothing + let _ = env_logger::init(); + start_web_server(None); + } +} diff --git a/src/web/page.rs b/src/web/page.rs new file mode 100644 index 000000000..6ccdb263c --- /dev/null +++ b/src/web/page.rs @@ -0,0 +1,101 @@ +//! Generic page struct + +use std::collections::BTreeMap; +use rustc_serialize::json::{Json, ToJson}; +use iron::{IronResult, Set, status}; +use iron::response::Response; +use handlebars_iron::Template; + + +pub struct Page { + title: Option, + content: T, + status: status::Status, + varss: BTreeMap, + varsb: BTreeMap, + varsi: BTreeMap, +} + + +impl Page { + pub fn new(content: T) -> Page { + Page { + title: None, + content: content, + status: status::Ok, + varss: BTreeMap::new(), + varsb: BTreeMap::new(), + varsi: BTreeMap::new(), + } + } + + /// Sets a string variable + pub fn set(mut self, var: &str, val: &str) -> Page { + &self.varss.insert(var.to_owned(), val.to_owned()); + self + } + + + /// Sets a boolean variable + pub fn set_bool(mut self, var: &str, val: bool) -> Page { + &self.varsb.insert(var.to_owned(), val); + self + } + + + /// Sets a boolean variable to true + pub fn set_true(mut self, var: &str) -> Page { + &self.varsb.insert(var.to_owned(), true); + self + } + + + /// Sets an integer variable + pub fn set_int(mut self, var: &str, val: i64) -> Page { + &self.varsi.insert(var.to_owned(), val); + self + } + + + /// Sets title of page + pub fn title(mut self, title: &str) -> Page { + self.title = Some(title.to_owned()); + self + } + + + /// Sets status code for response + pub fn set_status(mut self, s: status::Status) -> Page { + self.status = s; + self + } + + + pub fn to_resp(self, template: &str) -> IronResult { + let mut resp = Response::new(); + let status = self.status; + let temp = Template::new(template, self); + resp.set_mut(temp).set_mut(status); + Ok(resp) + } +} + + +impl ToJson for Page { + fn to_json(&self) -> Json { + let mut tree = BTreeMap::new(); + + if let Some(ref title) = self.title { + tree.insert("title".to_owned(), title.to_json()); + } + + tree.insert("content".to_owned(), self.content.to_json()); + tree.insert("cratesfyi_version".to_owned(), ::BUILD_VERSION.to_json()); + tree.insert("cratesfyi_version_safe".to_owned(), + ::BUILD_VERSION.replace(" ", "-").replace("(", "").replace(")", "").to_json()); + tree.insert("varss".to_owned(), self.varss.to_json()); + tree.insert("varsb".to_owned(), self.varsb.to_json()); + tree.insert("varsi".to_owned(), self.varsi.to_json()); + Json::Object(tree) + } +} diff --git a/src/web/pool.rs b/src/web/pool.rs new file mode 100644 index 000000000..cbd5c2b3b --- /dev/null +++ b/src/web/pool.rs @@ -0,0 +1,29 @@ + + +use iron::prelude::*; +use iron::{BeforeMiddleware, typemap}; +use r2d2; +use r2d2_postgres; +use db::create_pool; + + +pub struct Pool { + pool: r2d2::Pool, +} + +impl typemap::Key for Pool { + type Value = r2d2::PooledConnection; +} + +impl Pool { + pub fn new() -> Pool { + Pool { pool: create_pool() } + } +} + +impl BeforeMiddleware for Pool { + fn before(&self, req: &mut Request) -> IronResult<()> { + req.extensions.insert::(self.pool.get().unwrap()); + Ok(()) + } +} diff --git a/src/web/releases.rs b/src/web/releases.rs new file mode 100644 index 000000000..1842bcad0 --- /dev/null +++ b/src/web/releases.rs @@ -0,0 +1,415 @@ +//! Releases web handlers + + +use super::{duration_to_str, match_version}; +use super::error::Nope; +use super::page::Page; +use super::pool::Pool; +use iron::prelude::*; +use iron::status; +use router::Router; +use rustc_serialize::json::{Json, ToJson}; +use std::collections::BTreeMap; +use time; +use postgres::Connection; + + +/// Number of release in home page +const RELEASES_IN_HOME: i64 = 15; +/// Releases in /releases page +const RELEASES_IN_RELEASES: i64 = 30; + + +pub struct Release { + name: String, + version: String, + description: Option, + target_name: Option, + rustdoc_status: bool, + release_time: time::Timespec, + stars: i32, +} + + +impl Default for Release { + fn default() -> Release { + Release { + name: String::new(), + version: String::new(), + description: None, + target_name: None, + rustdoc_status: false, + release_time: time::get_time(), + stars: 0, + } + } +} + + +impl ToJson for Release { + fn to_json(&self) -> Json { + let mut m: BTreeMap = BTreeMap::new(); + m.insert("name".to_string(), self.name.to_json()); + m.insert("version".to_string(), self.version.to_json()); + m.insert("description".to_string(), self.description.to_json()); + m.insert("target_name".to_string(), self.target_name.to_json()); + m.insert("rustdoc_status".to_string(), self.rustdoc_status.to_json()); + m.insert("release_time".to_string(), + duration_to_str(self.release_time).to_json()); + m.insert("stars".to_string(), self.stars.to_json()); + m.to_json() + } +} + + +enum Order { + ReleaseTime, // this is default order + GithubStars, +} + + +fn get_releases(conn: &Connection, page: i64, limit: i64, order: Order) -> Vec { + + let offset = (page - 1) * limit; + + // TODO: This function changed so much during development and current version have code + // repeats for queries. There is definitely room for improvements. + let query = match order { + Order::ReleaseTime => { + "SELECT crates.name, \ + releases.version, \ + releases.description, \ + releases.target_name, \ + releases.release_time, \ + releases.rustdoc_status, \ + crates.github_stars \ + FROM crates \ + INNER JOIN releases ON crates.id = releases.crate_id \ + ORDER BY releases.release_time DESC \ + LIMIT $1 OFFSET $2" + } + Order::GithubStars => { + "SELECT crates.name, \ + releases.version, \ + releases.description, \ + releases.target_name, \ + releases.release_time, \ + releases.rustdoc_status, \ + crates.github_stars \ + FROM crates \ + INNER JOIN releases ON releases.id = crates.latest_version_id \ + ORDER BY crates.github_stars DESC \ + LIMIT $1 OFFSET $2" + } + }; + + let mut packages = Vec::new(); + for row in &conn.query(&query, &[&limit, &offset]).unwrap() { + let package = Release { + name: row.get(0), + version: row.get(1), + description: row.get(2), + target_name: row.get(3), + release_time: row.get(4), + rustdoc_status: row.get(5), + stars: row.get(6), + }; + + packages.push(package); + } + + packages +} + + + +fn get_releases_by_author(conn: &Connection, + page: i64, + limit: i64, + author: &str) + -> (String, Vec) { + + let offset = (page - 1) * limit; + + let query = "SELECT crates.name, + releases.version, + releases.description, + releases.target_name, + releases.release_time, + releases.rustdoc_status, + crates.github_stars, + authors.name + FROM crates + INNER JOIN releases ON releases.id = crates.latest_version_id + INNER JOIN author_rels ON releases.id = author_rels.rid + INNER JOIN authors ON authors.id = author_rels.aid + WHERE authors.slug = $1 + ORDER BY crates.github_stars DESC + LIMIT $2 OFFSET $3"; + + let mut author_name = String::new(); + let mut packages = Vec::new(); + for row in &conn.query(&query, &[&author, &limit, &offset]).unwrap() { + let package = Release { + name: row.get(0), + version: row.get(1), + description: row.get(2), + target_name: row.get(3), + release_time: row.get(4), + rustdoc_status: row.get(5), + stars: row.get(6), + }; + + author_name = row.get(7); + packages.push(package); + } + + (author_name, packages) +} + + + +fn get_search_results(conn: &Connection, + query: &str, + page: i64, + limit: i64) + -> Option<(i64, Vec)> { + + let offset = (page - 1) * limit; + let mut packages = Vec::new(); + + for row in &conn.query("SELECT crates.name, \ + releases.version, \ + releases.description, \ + releases.target_name, \ + releases.release_time, \ + releases.rustdoc_status, \ + ts_rank_cd(crates.content, to_tsquery($1)) AS rank \ + FROM crates \ + INNER JOIN releases ON crates.latest_version_id = releases.id \ + WHERE crates.content @@ to_tsquery($1) \ + ORDER BY rank DESC \ + LIMIT $2 OFFSET $3", + &[&query, &limit, &offset]) + .unwrap() { + + let package = Release { + name: row.get(0), + version: row.get(1), + description: row.get(2), + target_name: row.get(3), + release_time: row.get(4), + rustdoc_status: row.get(5), + ..Release::default() + }; + + packages.push(package); + } + + if !packages.is_empty() { + // get count of total results + let rows = conn.query("SELECT COUNT(*) FROM crates WHERE content @@ to_tsquery($1)", + &[&query]) + .unwrap(); + + Some((rows.get(0).get(0), packages)) + } else { + None + } +} + + + +pub fn home_page(req: &mut Request) -> IronResult { + let conn = req.extensions.get::().unwrap(); + let packages = get_releases(conn, 1, RELEASES_IN_HOME, Order::ReleaseTime); + Page::new(packages) + .set_true("show_search_form") + .set_true("hide_package_navigation") + .to_resp("releases") +} + + +pub fn releases_handler(req: &mut Request) -> IronResult { + // page number of releases + let page_number: i64 = req.extensions + .get::() + .unwrap() + .find("page") + .unwrap_or("1") + .parse() + .unwrap_or(1); + + let conn = req.extensions.get::().unwrap(); + let packages = get_releases(conn, page_number, RELEASES_IN_RELEASES, Order::ReleaseTime); + + if packages.is_empty() { + return Err(IronError::new(Nope::CrateNotFound, status::NotFound)); + } + + // Show next and previous page buttons + // This is a temporary solution to avoid expensive COUNT(*) + let (show_next_page, show_previous_page) = (packages.len() == RELEASES_IN_RELEASES as usize, + page_number != 1); + + Page::new(packages) + .title("Releases") + .set("description", "Recently uploaded crates") + .set("release_type", "recent") + .set_true("show_releases_navigation") + .set_true("releases_navigation_recent_tab") + .set_bool("show_next_page_button", show_next_page) + .set_int("next_page", page_number + 1) + .set_bool("show_previous_page_button", show_previous_page) + .set_int("previous_page", page_number - 1) + .to_resp("releases") +} + + +// TODO: This function is almost identical to previous one +pub fn stars_handler(req: &mut Request) -> IronResult { + // page number of releases + let page_number: i64 = req.extensions + .get::() + .unwrap() + .find("page") + .unwrap_or("1") + .parse() + .unwrap_or(1); + + let conn = req.extensions.get::().unwrap(); + let packages = get_releases(conn, page_number, RELEASES_IN_RELEASES, Order::GithubStars); + + if packages.is_empty() { + return Err(IronError::new(Nope::CrateNotFound, status::NotFound)); + } + + // Show next and previous page buttons + // This is a temporary solution to avoid expensive COUNT(*) + let (show_next_page, show_previous_page) = (packages.len() == RELEASES_IN_RELEASES as usize, + page_number != 1); + + Page::new(packages) + .title("Releases") + .set("description", "Most starred crates") + .set("release_type", "stars") + .set_true("show_releases_navigation") + .set_true("releases_navigation_stars_tab") + .set_true("show_stars") + .set_bool("show_next_page_button", show_next_page) + .set_int("next_page", page_number + 1) + .set_bool("show_previous_page_button", show_previous_page) + .set_int("previous_page", page_number - 1) + .to_resp("releases") +} + + +pub fn author_handler(req: &mut Request) -> IronResult { + // page number of releases + let page_number: i64 = req.extensions + .get::() + .unwrap() + .find("page") + .unwrap_or("1") + .parse() + .unwrap_or(1); + + let conn = req.extensions.get::().unwrap(); + let author = req.extensions.get::().unwrap().find("author"); + + if author.is_none() { + return Err(IronError::new(Nope::CrateNotFound, status::NotFound)); + } + + let (author_name, packages) = + get_releases_by_author(conn, page_number, RELEASES_IN_RELEASES, author.unwrap()); + + if packages.is_empty() { + return Err(IronError::new(Nope::CrateNotFound, status::NotFound)); + } + + // Show next and previous page buttons + // This is a temporary solution to avoid expensive COUNT(*) + let (show_next_page, show_previous_page) = (packages.len() == RELEASES_IN_RELEASES as usize, + page_number != 1); + + Page::new(packages) + .title("Releases") + .set("description", &format!("Crates from {}", author_name)) + .set("author", &author_name) + .set("release_type", &author.unwrap()) + .set_true("show_releases_navigation") + .set_true("show_stars") + .set_bool("show_next_page_button", show_next_page) + .set_int("next_page", page_number + 1) + .set_bool("show_previous_page_button", show_previous_page) + .set_int("previous_page", page_number - 1) + .to_resp("releases") +} + + +pub fn search_handler(req: &mut Request) -> IronResult { + use params::{Params, Value}; + + let params = req.get::().unwrap(); + let query = params.find(&["query"]); + + let conn = req.extensions.get::().unwrap(); + if let Some(&Value::String(ref query)) = query { + + // check if I am feeling lucky button pressed and redirect user to crate page + // if there is a match + // TODO: Redirecting to latest doc might be more useful + if params.find(&["i-am-feeling-lucky"]).is_some() { + if let Some(version) = match_version(&conn, &query, None) { + use iron::Url; + use iron::modifiers::Redirect; + let url = Url::parse(&format!("{}://{}:{}/crate/{}/{}", + req.url.scheme, + req.url.host, + req.url.port, + query, + version)[..]) + .unwrap(); + let mut resp = Response::with((status::Found, Redirect(url))); + + use iron::headers::{Expires, HttpDate}; + use time; + resp.headers.set(Expires(HttpDate(time::now()))); + return Ok(resp); + } + } + + + let search_query = query.replace(" ", " & "); + get_search_results(&conn, &search_query, 1, RELEASES_IN_RELEASES) + .ok_or(IronError::new(Nope::NoResults, status::NotFound)) + .and_then(|(_, results)| { + // FIXME: There is no pagination + Page::new(results) + .set("search_query", &query) + .title(&format!("Search results for '{}'", query)) + .to_resp("releases") + }) + } else { + Err(IronError::new(Nope::NoResults, status::NotFound)) + } +} + + +pub fn activity_handler(req: &mut Request) -> IronResult { + let conn = req.extensions.get::().unwrap(); + let release_activity_data: Json = + conn.query("SELECT value FROM config WHERE name = 'release_activity'", + &[]) + .unwrap() + .get(0) + .get(0); + Page::new(release_activity_data) + .title("Releases") + .set("description", "Monthly release activity") + .set_true("show_releases_navigation") + .set_true("releases_navigation_activity_tab") + .set_true("javascript_highchartjs") + .to_resp("releases_activity") +} diff --git a/src/web/rustdoc.rs b/src/web/rustdoc.rs new file mode 100644 index 000000000..e3233762b --- /dev/null +++ b/src/web/rustdoc.rs @@ -0,0 +1,209 @@ +//! rustdoc handler + + +use super::pool::Pool; +use super::file::File; +use super::MetaData; +use iron::prelude::*; +use iron::{status, Url}; +use iron::modifiers::Redirect; +use router::Router; +use super::match_version; +use super::error::Nope; +use super::page::Page; +use rustc_serialize::json::{Json, ToJson}; +use std::collections::BTreeMap; + + + +#[derive(Debug)] +struct RustdocPage { + pub head: String, + pub body: String, + pub name: String, + pub version: String, + pub description: Option, + pub metadata: Option, + pub platforms: Option +} + + +impl Default for RustdocPage { + fn default() -> RustdocPage { + RustdocPage { + head: String::new(), + body: String::new(), + name: String::new(), + version: String::new(), + description: None, + metadata: None, + platforms: None, + } + } +} + + +impl ToJson for RustdocPage { + fn to_json(&self) -> Json { + let mut m: BTreeMap = BTreeMap::new(); + m.insert("rustdoc_head".to_string(), self.head.to_json()); + m.insert("rustdoc_body".to_string(), self.body.to_json()); + m.insert("rustdoc_status".to_string(), true.to_json()); + m.insert("name".to_string(), self.name.to_json()); + m.insert("version".to_string(), self.version.to_json()); + m.insert("description".to_string(), self.description.to_json()); + m.insert("metadata".to_string(), self.metadata.to_json()); + m.insert("platforms".to_string(), self.platforms.to_json()); + m.to_json() + } +} + + + +pub fn rustdoc_redirector_handler(req: &mut Request) -> IronResult { + + fn redirect_to_doc(req: &Request, + name: &str, + vers: &str, + target_name: &str) + -> IronResult { + let url = Url::parse(&format!("{}://{}:{}/{}/{}/{}/", + req.url.scheme, + req.url.host, + req.url.port, + name, + vers, + target_name)[..]) + .unwrap(); + let mut resp = Response::with((status::Found, Redirect(url))); + + use iron::headers::{Expires, HttpDate}; + use time; + resp.headers.set(Expires(HttpDate(time::now()))); + + Ok(resp) + } + + // this handler should never called without crate pattern + let crate_name = req.extensions.get::().unwrap().find("crate").unwrap(); + let req_version = req.extensions.get::().unwrap().find("version"); + + let conn = req.extensions.get::().unwrap(); + + let version = match match_version(&conn, &crate_name, req_version) { + Some(v) => v, + None => return Err(IronError::new(Nope::CrateNotFound, status::NotFound)), + }; + + // get target name + // FIXME: This is a bit inefficient but allowing us to use less code in general + let target_name: String = conn.query("SELECT target_name FROM releases INNER JOIN crates ON \ + crates.id = releases.crate_id WHERE crates.name = $1 \ + AND releases.version = $2", + &[&crate_name, &version]) + .unwrap() + .get(0) + .get(0); + + redirect_to_doc(req, &crate_name, &version, &target_name) +} + + +pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult { + + let path = { + let mut path = req.url.path.clone(); + // documentations have "rustdoc" prefix in database + path.insert(0, "rustdoc".to_owned()); + + let mut path = path.join("/"); + if path.ends_with("/") { + path.push_str("index.html") + } + + path + }; + + // don't touch anything other than html files + if !path.ends_with(".html") { + return Err(IronError::new(Nope::ResourceNotFound, status::NotFound)); + } + + let conn = req.extensions.get::().unwrap(); + + let file = match File::from_path(&conn, &path) { + Some(f) => f, + None => return Err(IronError::new(Nope::ResourceNotFound, status::NotFound)), + }; + + let (mut in_head, mut in_body) = (false, false); + + let mut content = RustdocPage::default(); + + let file_content = String::from_utf8(file.content).unwrap(); + + for line in file_content.lines() { + + if line.starts_with("().unwrap().find("crate").unwrap_or("").to_string(); + let version = req.extensions + .get::() + .unwrap() + .find("version") + .unwrap_or("") + .to_string(); + + // content.metadata = MetaData::from_crate(&conn, &name, &version); + let (metadata, platforms) = { + let rows = conn.query("SELECT crates.name, + releases.version, + releases.description, + releases.target_name, + releases.rustdoc_status, + doc_targets + FROM releases + INNER JOIN crates ON crates.id = releases.crate_id + WHERE crates.name = $1 AND releases.version = $2", + &[&name, &version]).unwrap(); + + let metadata = MetaData { + name: rows.get(0).get(0), + version: rows.get(0).get(1), + description: rows.get(0).get(2), + target_name: rows.get(0).get(3), + rustdoc_status: rows.get(0).get(4), + }; + let platforms: Json = rows.get(0).get(5); + (Some(metadata), platforms) + }; + + content.metadata = metadata; + content.platforms = Some(platforms); + + Page::new(content) + .set_true("show_package_navigation") + .set_true("package_navigation_documentation_tab") + .set_true("package_navigation_show_platforms_tab") + .to_resp("rustdoc") +} diff --git a/src/web/source.rs b/src/web/source.rs new file mode 100644 index 000000000..a0ca99b20 --- /dev/null +++ b/src/web/source.rs @@ -0,0 +1,250 @@ +//! Source code browser + + +use std::collections::BTreeMap; +use std::cmp::Ordering; +use super::MetaData; +use super::page::Page; +use super::pool::Pool; +use super::file::File as DbFile; +use iron::prelude::*; +use router::Router; +use rustc_serialize::json::{Json, ToJson}; +use postgres::Connection; + + +#[derive(PartialEq, PartialOrd)] +enum FileType { + Dir, + Text, + Binary, + RustSource, +} + + +#[derive(PartialEq, PartialOrd)] +struct File { + name: String, + file_type: FileType, +} + + +struct FileList { + metadata: MetaData, + files: Vec, +} + + +impl ToJson for FileList { + fn to_json(&self) -> Json { + let mut m: BTreeMap = BTreeMap::new(); + + m.insert("metadata".to_string(), self.metadata.to_json()); + + let mut file_vec: Vec = Vec::new(); + + for file in &self.files { + let mut file_m: BTreeMap = BTreeMap::new(); + file_m.insert("name".to_string(), file.name.to_json()); + + file_m.insert(match file.file_type { + FileType::Dir => "file_type_dir".to_string(), + FileType::Text => "file_type_text".to_string(), + FileType::Binary => "file_type_binary".to_string(), + FileType::RustSource => "file_type_rust_source".to_string(), + }, + true.to_json()); + + file_vec.push(file_m.to_json()); + } + + m.insert("files".to_string(), file_vec.to_json()); + m.to_json() + } +} + + +impl FileList { + /// Gets FileList from a request path + /// + /// All paths stored in database have this format: + /// + /// ```text + /// [ + /// ["text/plain",".gitignore"], + /// ["text/x-c","src/reseeding.rs"], + /// ["text/x-c","src/lib.rs"], + /// ["text/x-c","README.md"], + /// ... + /// ] + /// ``` + /// + /// This function is only returning FileList for requested directory. If is empty, + /// it will return list of files (and dirs) for root directory. req_path must be a + /// directory or empty for root directory. + pub fn from_path(conn: &Connection, + name: &str, + version: &str, + req_path: &str) + -> Option { + + let rows = conn.query("SELECT crates.name, + releases.version, + releases.description, + releases.target_name, + releases.rustdoc_status, + releases.files + FROM releases + LEFT OUTER JOIN crates ON crates.id = releases.crate_id + WHERE crates.name = $1 AND releases.version = $2", + &[&name, &version]) + .unwrap(); + + if rows.len() == 0 { + return None; + } + + let files: Json = rows.get(0).get(5); + + let mut file_list: Vec = Vec::new(); + + files.as_array().map(|files| { + for file in files { + file.as_array().map(|file| { + let mime = file[0].as_string().unwrap(); + let path = file[1].as_string().unwrap(); + + // skip .cargo-ok generated by cargo + if path == ".cargo-ok" { + return; + } + + // look only files for req_path + if path.starts_with(&req_path) { + // remove req_path from path to reach files in this directory + let path = path.replace(&req_path, ""); + let path_splited: Vec<&str> = path.split("/").collect(); + + // if path have '/' it is a directory + let ftype = if path_splited.len() > 1 { + FileType::Dir + } else if mime.starts_with("text") && + path_splited[0].ends_with(".rs") { + FileType::RustSource + } else if mime.starts_with("text") { + FileType::Text + } else { + FileType::Binary + }; + + let file = File { + name: path_splited[0].to_owned(), + file_type: ftype, + }; + + // avoid adding duplicates, a directory may occur more than once + if !file_list.contains(&file) { + file_list.push(file); + } + } + }); + } + }); + + if file_list.is_empty() { + return None; + } + + file_list.sort_by(|a, b| { + // directories must be listed first + if a.file_type == FileType::Dir && b.file_type != FileType::Dir { + Ordering::Less + } else if a.file_type != FileType::Dir && b.file_type == FileType::Dir { + Ordering::Greater + } else { + a.name.to_lowercase().cmp(&b.name.to_lowercase()) + } + }); + + Some(FileList { + metadata: MetaData { + name: rows.get(0).get(0), + version: rows.get(0).get(1), + description: rows.get(0).get(2), + target_name: rows.get(0).get(3), + rustdoc_status: rows.get(0).get(4), + }, + files: file_list, + }) + } +} + + +pub fn source_browser_handler(req: &mut Request) -> IronResult { + let name = req.extensions.get::().unwrap().find("name").unwrap(); + let version = req.extensions.get::().unwrap().find("version").unwrap(); + + // get path (req_path) for FileList::from_path and actual path for super::file::File::from_path + let (req_path, file_path) = { + let mut req_path = req.url.path.clone(); + // remove first elements from path which is /crate/:name/:version/source + for _ in 0..4 { + req_path.remove(0); + } + let file_path = format!("sources/{}/{}/{}", name, version, req_path.join("/")); + + // FileList::from_path is only working for directories + // remove file name if it's not a directory + req_path.last_mut().map(|last| { + if !last.is_empty() { + last.clear(); + } + }); + + // remove crate name and version from req_path + let path = req_path.join("/").replace(&format!("{}/{}/", name, version), ""); + + (path, file_path) + }; + + + let conn = req.extensions.get::().unwrap(); + + // try to get actual file first + // skip if request is a directory + let file = if !file_path.ends_with("/") { + DbFile::from_path(&conn, &file_path) + } else { + None + }; + + + let (content, is_rust_source) = if let Some(file) = file { + // serve the file with DatabaseFileHandler if file isn't text and not empty + if !file.mime.starts_with("text") && !file.is_empty() { + return Ok(file.serve()); + } else if file.mime.starts_with("text") && !file.is_empty() { + (String::from_utf8(file.content).ok(), file.path.ends_with(".rs")) + } else { + (None, false) + } + } else { + (None, false) + }; + + let list = FileList::from_path(&conn, &name, &version, &req_path); + + let page = Page::new(list) + .set_bool("show_parent_link", !req_path.is_empty()) + .set_true("javascript_highlightjs") + .set_true("show_package_navigation") + .set_true("package_source_tab"); + + if let Some(content) = content { + page.set("file_content", &content) + .set_bool("file_content_rust_source", is_rust_source) + .to_resp("source") + } else { + page.to_resp("source") + } +} diff --git a/templates/about.hbs b/templates/about.hbs new file mode 100644 index 000000000..66e6147eb --- /dev/null +++ b/templates/about.hbs @@ -0,0 +1,86 @@ +{{> header}} + +
+

+ Docs.rs (formerly cratesfyi) is an open source project to host + documentation of crates for the Rust Programming Language. +

+ +

+ Docs.rs automatically builds crates' documentation released on + crates.io + using the nightly release of the Rust compiler. +

+ +

+ The source code of Docs.rs is available on + Github. If + you ever encounter an issue, don't hesitate to report it! (And + thanks in advance!) +

+ +

+ The README of a crate is taken from the readme field defined in + Cargo.toml. If a crate doesn't have this field, + no README will be displayed. +

+ +

Redirections

+ +

+ Docs.rs is using semver to parse URLs. You can use this feature to access + crates' documentation easily. Example of URL redirections for + clap crate: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
URLRedirects to documentation of
docs.rs/clapLatest version of clap
docs.rs/clap/^22.* version
docs.rs/clap/^2.92.9.* version
docs.rs/clap/2.9.32.9.3 version (you don't need = unlike semver)
+ +

The crates.fyi domain will redirect to + docs.rs, supporting all of the redirects discussed + above

+ +

Version

+

Currently running Docs.rs version is: {{cratesfyi_version}} + +

Contributors

+ + +

Sponsors

+

Hosting generously provided by:

+

Powered by LeaseWeb

+

If you are interested in sponsoring Docs.rs, please don't hesitate to contact us at onur@onur.im

+
+ +{{> footer}} diff --git a/templates/builds.hbs b/templates/builds.hbs new file mode 100644 index 000000000..843cce49e --- /dev/null +++ b/templates/builds.hbs @@ -0,0 +1,43 @@ +{{> header}} + +{{#with content}} + +{{/with}} + +{{> footer}} diff --git a/templates/crate_details.hbs b/templates/crate_details.hbs new file mode 100644 index 000000000..e8570b89f --- /dev/null +++ b/templates/crate_details.hbs @@ -0,0 +1,70 @@ +{{> header}} + + +{{#with content}} +
+
+
+
+ +
+
+
+ {{#unless is_library}} +
{{name}}-{{version}} is not a library.
+ {{else}} + {{#unless build_status}} +
docs.rs failed to build {{name}}-{{version}}
Please check build logs and if you believe this is docs.rs' fault, report into this issue report.
+ {{else}} + {{#unless rustdoc_status}} +
{{name}}-{{version}} doesn't have any documentation.
+ {{/unless}} + {{/unless}} + {{/unless}} + {{#if readme}} + {{{readme}}} + {{else}} + {{{rustdoc}}} + {{/if}} +
+
+ +
+{{/with}} + + +{{> footer}} diff --git a/templates/error.hbs b/templates/error.hbs new file mode 100644 index 000000000..bf2fd7d93 --- /dev/null +++ b/templates/error.hbs @@ -0,0 +1,2 @@ +{{> header}} +{{> footer}} diff --git a/templates/footer.hbs b/templates/footer.hbs new file mode 100644 index 000000000..aca7519a0 --- /dev/null +++ b/templates/footer.hbs @@ -0,0 +1,3 @@ +{{#if varsb.javascript_highlightjs}}{{/if}} + + diff --git a/templates/header.hbs b/templates/header.hbs new file mode 100644 index 000000000..fcbd127f0 --- /dev/null +++ b/templates/header.hbs @@ -0,0 +1,25 @@ + + + + + + + + + + + + + {{#if varsb.javascript_highlightjs}} + + + + {{/if}} + {{#if varsb.javascript_highchartjs}} + + {{/if}} + {{#if title}}{{title}} - {{/if}}{{#if content.metadata.name}}{{content.metadata.name}} {{content.metadata.version}} - {{/if}}Docs.rs + + + + {{> navigation}} diff --git a/templates/navigation.hbs b/templates/navigation.hbs new file mode 100644 index 000000000..d40c208a0 --- /dev/null +++ b/templates/navigation.hbs @@ -0,0 +1,79 @@ + + + {{#unless varsb.hide_package_navigation}} +
+
+

{{#if title}}{{title}}{{else}}{{content.metadata.name}} {{content.metadata.version}}{{/if}}

+
{{#if content.metadata.description }}{{content.metadata.description}}{{else}}{{varss.description}}{{/if}}
+ + {{#if ../varsb.show_package_navigation}} +
+ {{#if ../varsb.package_navigation_show_platforms_tab}} + + {{/if}} + {{#with content.metadata}} + + {{/with}} +
+ {{/if}} + + {{#if varsb.show_releases_navigation}} +
+ +
+ {{/if}} +
+
+ {{/unless}} diff --git a/templates/releases.hbs b/templates/releases.hbs new file mode 100644 index 000000000..8041a2c0a --- /dev/null +++ b/templates/releases.hbs @@ -0,0 +1,66 @@ +{{> header}} + +{{#if varsb.show_search_form}} +
+

Docs.rs

+ +
+
+
+ + +
+
+ +
+{{/if}} + +
+
+ {{#if varsb.show_search_form}} +
+ Recent Releases +
+ {{/if}} + + + {{#unless varsb.show_search_form}} + + {{/unless}} +
+
+ +{{> footer}} diff --git a/templates/releases_activity.hbs b/templates/releases_activity.hbs new file mode 100644 index 000000000..1137aa989 --- /dev/null +++ b/templates/releases_activity.hbs @@ -0,0 +1,38 @@ +{{> header}} +{{#with content}} + +
+
+
+ + +{{/with}} +{{> footer}} diff --git a/templates/rustdoc.hbs b/templates/rustdoc.hbs new file mode 100644 index 000000000..77f8da1c0 --- /dev/null +++ b/templates/rustdoc.hbs @@ -0,0 +1,15 @@ + + + + {{{content.rustdoc_head}}} + + + + + + {{> navigation}} +
+ {{{content.rustdoc_body}}} +
+ + diff --git a/templates/source.hbs b/templates/source.hbs new file mode 100644 index 000000000..8ab5a72ad --- /dev/null +++ b/templates/source.hbs @@ -0,0 +1,38 @@ +{{> header}} + + +{{#with content}} +
+
+ + {{#if ../varss.file_content}} +
+
{{ ../varss.file_content }}
+
+ {{/if}} +
+
+{{/with}} + +{{> footer}} diff --git a/templates/style.scss b/templates/style.scss new file mode 100644 index 000000000..729d1c22a --- /dev/null +++ b/templates/style.scss @@ -0,0 +1,553 @@ + +// FONTS +$font-family-sans: "Fira Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; +$font-family-serif: "Source Serif Pro",Georgia,Times,"Times New Roman",serif; +$font-family-mono: "Source Code Pro", Menlo, Monaco, Consolas, "DejaVu Sans Mono", Inconsolata, monospace; + + +// COLORS - Guess what? +$color-standard: #000; // pure black +$color-url: #4d76ae; // blue +$color-macro: #068000; // green +$color-struct: #df3600; // red +$color-enum: #5e9766; // light green +$color-type: #e57300; // orange +$color-keyword: #8959A8; // purple +$color-string: #718C00; // greenish +$color-macro-in-code: #3E999F; // blueish +$color-lifetime-incode: #B76514; // orangish +$color-comment-in-code: #8E908C; // light gray +$color-background-code: #F5F5F5; // lighter gray +$color-border: #ddd; // gray + + + +// pure compatible media queries +$media-sm: "screen and (min-width: 35.5em)"; +$media-md: "screen and (min-width: 48em)"; +$media-lg: "screen and (min-width: 64em)"; +$media-xl: "screen and (min-width: 80em)"; +// usage: +// @media #{$media-sm} { ... } + + +html, button, input, select, textarea, +.pure-g [class *= "pure-u"] { + font-family: $font-family-sans; + color: $color-standard; +} + +strong { + font-weight: 500; +} + +.pure-button-normal { + background-color: #fff; + box-sizing: border-box !important; + border: 1px solid $color-border; +} + +.description { + font-family: $font-family-serif; +} + +// rustdoc overrides +div.rustdoc { + font-family: $font-family-serif; + padding: 0 1em; + + @media #{$media-sm} { + padding-left: 0; + } +} + +.sidebar { + top: 160px; + left: auto; +} + +body { + padding: 0; + margin: 0; +} + + +// rustdocs have 200px sidebar and +// max-width 960px main pane +// BUT I really want to make the website centered + +body { + text-align: center; + font: 16px/1.4 $font-family-sans; +} + +div.container { + max-width: 1160px; + margin: 0 auto; + text-align: left; +} + +div.nav-container { + border-bottom: 1px solid $color-border; + + li { + border-left: 1px solid $color-border; + } + + .pure-menu-has-children>.pure-menu-link:after { + font-size: 14px; + } + + a { + font-size: 14px; + font-weight: 400; + } + + .pure-menu-link:hover { + color: $color-standard; + background-color: inherit; + } + + form.landing-search-form-nav { + input.search-input-nav { + float: right; + max-width: 200px; + border: none; + margin: 0 1em 0 0; + font-size: 14px; + text-align: right; + box-shadow: none; + height: 33px; + display: none; + @media #{$media-sm} { + display: block; + } + } + input.search-input-nav:focus { + outline: unset; + } + } + + .pure-menu-children { + border: 1px solid $color-border; + border-radius: 0 0 2px 2px; + margin-left: -1px; + li { + border-left: none; + } + } +} + +div.landing { + text-align: center; + padding-top: 30px; + padding-bottom: 60px; + + h1.brand { + font-size: 3em; + margin-bottom: 10px; + } + + form.landing-search-form { + max-width: 500px; + margin: 0 auto; + padding: .4em 1em; + + div.buttons { + margin-top: 30px; + } + } + +} + + + +div.recent-releases-container { + text-align: left; + margin-bottom: 50px; + + ul, li { + list-style-type: none; + margin: 0; + padding: 0; + } + + .release { + display: block; + border-bottom: 1px solid $color-border; + padding: .4em 1em; + + @media #{$media-lg} { + padding: .4em 0; + margin: 0 1em; + } + } + + .release:hover { + background-color: $color-background-code; + } + + li:last-child .release { + border-bottom: none; + } + + .name { + color: $color-url; + font-weight: 500; + } + + .build { + font-weight: 500; + i.fa-check { + color: $color-macro; + } + i.fa-close { + color: $color-struct; + + } + } + + .description { + font-family: $font-family-serif; + font-weight: normal; + @media #{$media-sm} { + font-size: 1em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .description:hover { + @media #{$media-sm} { + overflow: visible; + white-space: normal; + } + } + + .date { + font-weight: normal; + @media #{$media-sm} { + text-align: right; + } + } + + div.pagination { + text-align: center; + margin: 1em; + + .pure-button { + margin: 0; + } + } +} + + +div.package-container { + background-color: $color-url; + color: $color-background-code;; + + h1 { + margin: 0; + padding: 20px 0 0 16px; + } + p { + margin: 0; + padding: 0 0 20px 16px; + } + + .pure-menu { + + .pure-menu-link { + background-color: #fff; + border-top: 1px solid $color-border; + border-left: 1px solid $color-border; + border-right: 1px solid $color-border; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom: 2px solid $color-border; + padding: .4em 1em; + } + + .pure-menu-active { + border-bottom: 2px solid #fff; + color: $color-standard; + } + + .pure-menu-link:hover { + color: $color-standard; + } + } +} + + +div.package-sheet-container { + margin-top: 10px; + margin-bottom: 20px; + + .pure-menu-link { + border-radius: 4px; + padding: .2em .8em; + font-weight: 400; + } + + .build-success { + color: $color-macro; + } + + .build-fail { + color: $color-struct; + } +} + +div.package-page-container { + div.package-menu { + padding: 0 10px; + + li.pure-menu-heading { + font-size: 1.3em; + color: #000; + font-weight: 500; + text-align: center; + border-bottom: 1px solid lighten($color-border, 5%); + text-transform: none; + padding-bottom: 6px; + margin: 20px 5px 15px 5px; + } + + li.pure-menu-heading:first-child { + margin-top: 0; + } + + li i.fa { + display: inline-block; + width: 20px; + } + + a.pure-menu-link { + font-size: 14px; + color: $color-standard; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 7px 8px; + } + + a.pure-menu-link:hover { + background-color: $color-background-code; + } + + div.sub-menu { + max-height: 135px; + overflow-y: auto; + + ul.pure-menu-list { + border-top: none; + } + + li.pure-menu-item:last-child { + border-bottom: none; + } + } + } + + div.package-details { + padding: 0 1em; + + font-family: $font-family-serif; + + a { + color: $color-url; + } + + a:hover { + text-decoration: underline; + } + + h1, h2, h3, h4, h5, h6 { + font-family: $font-family-sans; + } + + h1:first-child, + h2:first-child, + h3:first-child, + h4:first-child, + h5:first-child, + h6:first-child { + margin-top: 0; + } + } + + pre { + background-color: inherit; + padding: 0; + + code { + white-space: pre; + } + } +} + + + +div.cratesfyi-package-container { + text-align: left; + background-color: $color-background-code; + border-bottom: 1px solid $color-border; + margin-bottom: 20px; + + h1 { + margin: 0; + padding: 15px 0 0 14px; + } + div.description { + font-family: $font-family-serif; + margin: 0; + padding: 0 0 15px 14px; + + @media #{$media-sm} { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .pure-menu { + margin-bottom: -1px; + padding-left: 14px; + + .pure-menu-link { + color: #666; + font-size: 14px; + padding: .4em 1em .3em 1em; + + .title { + display: none; + @media #{$media-sm} { + display: inline; + } + } + } + + .pure-menu-active { + color: $color-standard; + + background-color: #fff; + border-top: 1px solid $color-border; + border-left: 1px solid $color-border; + border-right: 1px solid $color-border; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom: 2px solid #fff; + } + + .pure-menu-active:hover { + background-color: #fff !important; + } + + .pure-menu-link:hover { + color: #000; + background-color: inherit; + } + } + + ul.platforms-menu { + float: right; + display: none; + ul.pure-menu-children { + left: auto; + right: 0; + border: 1px solid $color-border; + border-radius: 2px; + } + + .pure-menu-has-children>.pure-menu-link:after { + font-size: 14px; + } + + @media #{$media-sm} { + display: inline-block; + } + } +} + + +div.warning { + font-family: $font-family-sans; + border-radius: 4px; + background-color: lighten($color-type, 45%); + padding: .4em 1em; + text-align: center; + margin-bottom: 10px; + a { + color: $color-url; + text-decoration: underline; + } +} + + +div.search-page-search-form { + padding: .4em 1em; + text-align: center; + + input.search-input { + display: inline-block; + max-width: 300px; + padding: .4em 1em; + } +} + +.menu-item-divided { + border-bottom: 1px solid $color-border; +} + +.rust-navigation-item { + background: url(/rust-logo.png) no-repeat; + background-position: 15px 45%; + background-size: 12px; + padding-left: 35px; +} + + +footer { + margin-top: 60px; + margin-bottom: 10px; + text-align: center; + ul { + list-style-type: none; + li { + list-style-type: none; + margin-bottom: 10px; + + a { + font-weight: 500; + color: $color-url; + } + } + li.hosting { + font-size: .8em; + } + } +} + + +.about { + font-family: $font-family-serif; + padding: .4em 1em; + a { + color: $color-url; + } + a:hover { + text-decoration: underline; + } + table { + margin-bottom: 10px; + } + thead tr th { + font-family: $font-family-sans; + font-weight: 500; + } + strong { + font-weight: bold; + } +}