diff --git a/Cargo.lock b/Cargo.lock index c3949548f..3f4a0b390 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,14 @@ name = "adler32" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "aho-corasick" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "aho-corasick" version = "0.7.10" @@ -366,6 +374,7 @@ dependencies = [ "failure 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "git2 0.13.6 (registry+https://github.com/rust-lang/crates.io-index)", + "handlebars-iron 0.25.2 (registry+https://github.com/rust-lang/crates.io-index)", "iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "kuchiki 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -984,6 +993,34 @@ name = "half" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "handlebars" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "pest 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "handlebars-iron" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "handlebars 0.29.1 (registry+https://github.com/rust-lang/crates.io-index)", + "iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", + "walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "heck" version = "0.3.1" @@ -1925,6 +1962,11 @@ dependencies = [ "plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "pest" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "pest" version = "1.0.6" @@ -2483,6 +2525,18 @@ dependencies = [ "rust-argon2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "regex" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "regex" version = "1.3.7" @@ -2502,6 +2556,14 @@ dependencies = [ "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "regex-syntax" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ucd-util 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "regex-syntax" version = "0.6.17" @@ -2708,6 +2770,15 @@ name = "safemem" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "same-file" +version = "0.1.3" +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.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "same-file" version = "1.0.6" @@ -3230,6 +3301,14 @@ name = "thin-slice" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "thread_local" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "thread_local" version = "1.0.1" @@ -3577,6 +3656,11 @@ name = "ucd-trie" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "ucd-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "unic-char-property" version = "0.9.0" @@ -3728,6 +3812,11 @@ name = "utf-8" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "utf8-ranges" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "vcpkg" version = "0.2.8" @@ -3753,6 +3842,16 @@ name = "void" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "walkdir" +version = "1.0.7" +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)", + "same-file 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "walkdir" version = "2.3.1" @@ -3960,6 +4059,7 @@ dependencies = [ "checksum ab_glyph_rasterizer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2b7e4e8cf778db814365e46839949ca74df4efb10e87ba4913e6ec5967ef0285" "checksum addr2line 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a49806b9dadc843c61e7c97e72490ad7f7220ae249012fbda9ad0609457c0543" "checksum adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" +"checksum aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5" "checksum aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" "checksum arc-swap 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b585a98a234c46fc563103e9278c9391fde1f4e6850334da895d27edb9580f62" @@ -4065,6 +4165,8 @@ dependencies = [ "checksum h2 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)" = "a5b34c246847f938a410a03c5458c7fee2274436675e76d8b903c08efc29c462" "checksum h2 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "79b7246d7e4b979c03fa093da39cfb3617a96bbeee6310af63991668d7e843ff" "checksum half 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d36fab90f82edc3c747f9d438e06cf0a491055896f2a279638bb5beed6c40177" +"checksum handlebars 0.29.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fb04af2006ea09d985fef82b81e0eb25337e51b691c76403332378a53d521edc" +"checksum handlebars-iron 0.25.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7b8ad7259b5bfcc65da1f1f3525eb5e4d5c4c6c7ce2d3b9c9945165e7e083c9c" "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" "checksum hermit-abi 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "91780f809e750b0a89f5544be56617ff6b1227ee485bcb06ebe10cdf89bd3b71" "checksum hex 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d6a22814455d41612f41161581c2883c0c6a1c41852729b17d5ed88f01e153aa" @@ -4166,6 +4268,7 @@ dependencies = [ "checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" "checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" "checksum persistent 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8e8fa0009c4f3d350281309909c618abddf10bb7e3145f28410782f6a5ec74c5" +"checksum pest 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0a6dda33d67c26f0aac90d324ab2eb7239c819fc7b2552fe9faa4fe88441edc8" "checksum pest 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0fce5d8b5cc33983fc74f78ad552b5522ab41442c4ca91606e4236eb4b5ceefc" "checksum pest 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" "checksum pest_derive 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3294f437119209b084c797604295f40227cffa35c57220b1e99a6ff3bf8ee4" @@ -4227,8 +4330,10 @@ dependencies = [ "checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" "checksum redox_users 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" +"checksum regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384" "checksum regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" "checksum regex-automata 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" +"checksum regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" "checksum regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" "checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" "checksum reqwest 0.10.6 (registry+https://github.com/rust-lang/crates.io-index)" = "3b82c9238b305f26f53443e3a4bc8528d64b8d0bee408ec949eb7bf5635ec680" @@ -4247,6 +4352,7 @@ dependencies = [ "checksum ryu 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ed3d612bc64430efeb3f7ee6ef26d590dce0c43249217bddc62112540c7941e1" "checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" "checksum safemem 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" +"checksum same-file 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d931a44fdaa43b8637009e7632a02adc4f2b2e0733c08caa4cf00e8da4a117a7" "checksum same-file 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" "checksum sass-rs 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cabcf7c6e55053f359911187ac401409aad2dc14338cae972dec266fee486abd" "checksum sass-sys 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)" = "dd454d3c8fa19fe6c66df5d6ced4933f3a40b29d5875114eacc469451136226d" @@ -4304,6 +4410,7 @@ dependencies = [ "checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum thin-slice 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" +"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" "checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" "checksum time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" "checksum tinytemplate 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d3dc76004a03cec1c5932bca4cdc2e39aaa798e3f82363dd94f9adf6098c12f" @@ -4337,6 +4444,7 @@ dependencies = [ "checksum typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6" "checksum typenum 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" "checksum ucd-trie 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +"checksum ucd-util 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "c85f514e095d348c279b1e5cd76795082cf15bd59b93207832abe0b1d8fed236" "checksum unic-char-property 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" "checksum unic-char-range 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" "checksum unic-common 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" @@ -4358,11 +4466,13 @@ dependencies = [ "checksum url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" "checksum urlencoded 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0a52f50139118b60ae91af08bf15ed158817d34b91b9d24c11ffbe21195d33e3" "checksum utf-8 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)" = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" +"checksum utf8-ranges 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba" "checksum vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" "checksum vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" "checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +"checksum walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "bb08f9e670fab86099470b97cd2b252d6527f0b3cc1401acdb595ffc9dd288ff" "checksum walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" "checksum want 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6395efa4784b027708f7451087e647ec73cc74f5d9bc2e418404248d679a230" "checksum want 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" diff --git a/Cargo.toml b/Cargo.toml index 681a49428..1526c1b73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ serde_json = "1.0" # iron dependencies iron = "0.5" router = "0.5" +handlebars-iron = "0.25" params = "0.8" staticfile = { version = "0.4", features = [ "cache" ] } tempfile = "3.1.0" diff --git a/dockerfiles/Dockerfile b/dockerfiles/Dockerfile index 206a578e1..d5f79bba9 100644 --- a/dockerfiles/Dockerfile +++ b/dockerfiles/Dockerfile @@ -74,6 +74,7 @@ RUN mkdir -p /opt/docsrs/prefix COPY --from=build /build/target/release/cratesfyi /usr/local/bin COPY static /opt/docsrs/prefix/public_html COPY templates /opt/docsrs/templates +COPY tera-templates /opt/docsrs/tera-templates COPY dockerfiles/entrypoint.sh /opt/docsrs/ WORKDIR /opt/docsrs diff --git a/src/build_queue.rs b/src/build_queue.rs index b8cbe6116..e3c36b92c 100644 --- a/src/build_queue.rs +++ b/src/build_queue.rs @@ -3,7 +3,7 @@ use crate::db::Pool; use crate::error::Result; use log::error; -#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize)] +#[derive(Debug, Eq, PartialEq, serde::Serialize)] pub(crate) struct QueuedCrate { #[serde(skip)] id: i32, diff --git a/src/docbuilder/limits.rs b/src/docbuilder/limits.rs index e641afdfc..39d3cba95 100644 --- a/src/docbuilder/limits.rs +++ b/src/docbuilder/limits.rs @@ -1,7 +1,7 @@ use crate::error::Result; use postgres::Connection; use serde::Serialize; -use std::time::Duration; +use std::{collections::BTreeMap, time::Duration}; #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub(crate) struct Limits { @@ -67,6 +67,52 @@ impl Limits { pub(crate) fn targets(&self) -> usize { self.targets } + + pub(crate) fn for_website(&self) -> BTreeMap { + let mut res = BTreeMap::new(); + res.insert("Available RAM".into(), SIZE_SCALE(self.memory)); + res.insert( + "Maximum rustdoc execution time".into(), + TIME_SCALE(self.timeout.as_secs() as usize), + ); + res.insert( + "Maximum size of a build log".into(), + SIZE_SCALE(self.max_log_size), + ); + if self.networking { + res.insert("Network access".into(), "allowed".into()); + } else { + res.insert("Network access".into(), "blocked".into()); + } + res.insert( + "Maximum number of build targets".into(), + self.targets.to_string(), + ); + res + } +} + +const TIME_SCALE: fn(usize) -> String = |v| scale(v, 60, &["seconds", "minutes", "hours"]); +const SIZE_SCALE: fn(usize) -> String = |v| scale(v, 1024, &["bytes", "KB", "MB", "GB"]); + +fn scale(value: usize, interval: usize, labels: &[&str]) -> String { + let (mut value, interval) = (value as f64, interval as f64); + let mut chosen_label = &labels[0]; + for label in &labels[1..] { + if value / interval >= 1.0 { + chosen_label = label; + value /= interval; + } else { + break; + } + } + // 2.x + let mut value = format!("{:.1}", value); + // 2.0 -> 2 + if value.ends_with(".0") { + value.truncate(value.len() - 2); + } + format!("{} {}", value, chosen_label) } #[cfg(test)] @@ -115,4 +161,56 @@ mod test { Ok(()) }); } + + #[test] + fn display_limits() { + let limits = Limits { + memory: 102_400, + timeout: Duration::from_secs(300), + targets: 1, + ..Limits::default() + }; + let display = limits.for_website(); + assert_eq!(display.get("Network access"), Some(&"blocked".into())); + assert_eq!( + display.get("Maximum size of a build log"), + Some(&"100 KB".into()) + ); + assert_eq!( + display.get("Maximum number of build targets"), + Some(&limits.targets.to_string()) + ); + assert_eq!( + display.get("Maximum rustdoc execution time"), + Some(&"5 minutes".into()) + ); + assert_eq!(display.get("Available RAM"), Some(&"100 KB".into())); + } + + #[test] + fn scale_limits() { + // time + assert_eq!(TIME_SCALE(300), "5 minutes"); + assert_eq!(TIME_SCALE(1), "1 seconds"); + assert_eq!(TIME_SCALE(7200), "2 hours"); + + // size + assert_eq!(SIZE_SCALE(1), "1 bytes"); + assert_eq!(SIZE_SCALE(100), "100 bytes"); + assert_eq!(SIZE_SCALE(1024), "1 KB"); + assert_eq!(SIZE_SCALE(10240), "10 KB"); + assert_eq!(SIZE_SCALE(1_048_576), "1 MB"); + assert_eq!(SIZE_SCALE(10_485_760), "10 MB"); + assert_eq!(SIZE_SCALE(1_073_741_824), "1 GB"); + assert_eq!(SIZE_SCALE(10_737_418_240), "10 GB"); + assert_eq!(SIZE_SCALE(std::u32::MAX as usize), "4 GB"); + + // fractional sizes + assert_eq!(TIME_SCALE(90), "1.5 minutes"); + assert_eq!(TIME_SCALE(5400), "1.5 hours"); + + assert_eq!(SIZE_SCALE(1_288_490_189), "1.2 GB"); + assert_eq!(SIZE_SCALE(3_758_096_384), "3.5 GB"); + assert_eq!(SIZE_SCALE(1_048_051_712), "999.5 MB"); + } } diff --git a/src/web/builds.rs b/src/web/builds.rs index f4bc80e6e..1e0fe9934 100644 --- a/src/web/builds.rs +++ b/src/web/builds.rs @@ -1,30 +1,42 @@ -use crate::{ - db::Pool, - docbuilder::Limits, - impl_webpage, - web::{page::WebPage, MetaData}, -}; +use super::duration_to_str; +use super::page::Page; +use super::MetaData; +use crate::db::Pool; +use crate::docbuilder::Limits; use chrono::{DateTime, NaiveDateTime, Utc}; -use iron::{ - headers::{ - AccessControlAllowOrigin, CacheControl, CacheDirective, ContentType, Expires, HttpDate, - }, - status, IronResult, Request, Response, -}; +use iron::prelude::*; use router::Router; -use serde::Serialize; +use serde::ser::{Serialize, SerializeStruct, Serializer}; -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct Build { id: i32, rustc_version: String, - docsrs_version: String, + cratesfyi_version: String, build_status: bool, build_time: DateTime, output: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +impl Serialize for Build { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("Build", 7)?; + state.serialize_field("id", &self.id)?; + state.serialize_field("rustc_version", &self.rustc_version)?; + state.serialize_field("cratesfyi_version", &self.cratesfyi_version)?; + state.serialize_field("build_status", &self.build_status)?; + state.serialize_field("build_time", &self.build_time.format("%+").to_string())?; // RFC 3339 + state.serialize_field("build_time_relative", &duration_to_str(self.build_time))?; + state.serialize_field("output", &self.output)?; + + state.end() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] struct BuildsPage { metadata: Option, builds: Vec, @@ -32,23 +44,32 @@ struct BuildsPage { limits: Limits, } -impl_webpage! { - BuildsPage = "crate/builds.html", +impl Serialize for BuildsPage { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("Buildspage", 4)?; + state.serialize_field("metadata", &self.metadata)?; + state.serialize_field("builds", &self.builds)?; + state.serialize_field("build_details", &self.build_details)?; + state.serialize_field("limits", &self.limits.for_website())?; + + state.end() + } } pub fn build_list_handler(req: &mut Request) -> IronResult { let router = extension!(req, Router); - let name = cexpect!(req, router.find("name")); - let version = cexpect!(req, router.find("version")); + let name = cexpect!(router.find("name")); + let version = cexpect!(router.find("version")); let req_build_id: i32 = router.find("id").unwrap_or("0").parse().unwrap_or(0); let conn = extension!(req, Pool).get()?; - let limits = ctry!(req, Limits::for_crate(&conn, name)); + let limits = ctry!(Limits::for_crate(&conn, name)); - let query = ctry!( - req, - conn.query( - "SELECT crates.name, + let query = ctry!(conn.query( + "SELECT crates.name, releases.version, releases.description, releases.rustdoc_status, @@ -64,24 +85,23 @@ pub fn build_list_handler(req: &mut Request) -> IronResult { INNER JOIN crates ON releases.crate_id = crates.id WHERE crates.name = $1 AND releases.version = $2 ORDER BY id DESC", - &[&name, &version] - ) - ); + &[&name, &version] + )); let mut build_details = None; // FIXME: getting builds.output may cause performance issues when release have tons of builds - let mut builds = query + let mut build_list = query .into_iter() .map(|row| { - let id: i32 = row.get("id"); + let id: i32 = row.get(5); let build = Build { id, - rustc_version: row.get("rustc_version"), - docsrs_version: row.get("cratesfyi_version"), - build_status: row.get("build_status"), - build_time: DateTime::from_utc(row.get::<_, NaiveDateTime>("build_time"), Utc), - output: row.get("output"), + rustc_version: row.get(6), + cratesfyi_version: row.get(7), + build_status: row.get(8), + build_time: DateTime::from_utc(row.get::<_, NaiveDateTime>(9), Utc), + output: row.get(10), }; if id == req_build_id { @@ -93,13 +113,19 @@ pub fn build_list_handler(req: &mut Request) -> IronResult { .collect::>(); if req.url.path().join("/").ends_with(".json") { + use iron::headers::{ + AccessControlAllowOrigin, CacheControl, CacheDirective, ContentType, Expires, HttpDate, + }; + use iron::status; + // Remove build output from build list for json output - for build in builds.iter_mut() { + for build in build_list.as_mut_slice() { build.output = None; } - let mut resp = Response::with((status::Ok, serde_json::to_string(&builds).unwrap())); - resp.headers.set(ContentType::json()); + let mut resp = Response::with((status::Ok, serde_json::to_string(&build_list).unwrap())); + resp.headers + .set(ContentType("application/json".parse().unwrap())); resp.headers.set(Expires(HttpDate(time::now()))); resp.headers.set(CacheControl(vec![ CacheDirective::NoCache, @@ -107,15 +133,175 @@ pub fn build_list_handler(req: &mut Request) -> IronResult { CacheDirective::MustRevalidate, ])); resp.headers.set(AccessControlAllowOrigin::Any); - Ok(resp) } else { - BuildsPage { + let builds_page = BuildsPage { metadata: MetaData::from_crate(&conn, &name, &version), - builds, + builds: build_list, build_details, limits, - } - .into_response(req) + }; + Page::new(builds_page) + .set_true("show_package_navigation") + .set_true("package_navigation_builds_tab") + .to_resp("builds") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::Utc; + use serde_json::json; + + #[test] + fn serialize_build() { + let time = Utc::now(); + let mut build = Build { + id: 22, + rustc_version: "rustc 1.43.0 (4fb7144ed 2020-04-20)".to_string(), + cratesfyi_version: "docsrs 0.6.0 (3dd32ec 2020-05-01)".to_string(), + build_status: true, + build_time: time, + output: None, + }; + + let correct_json = json!({ + "id": 22, + "rustc_version": "rustc 1.43.0 (4fb7144ed 2020-04-20)", + "cratesfyi_version": "docsrs 0.6.0 (3dd32ec 2020-05-01)", + "build_time": time.format("%+").to_string(), + "build_time_relative": duration_to_str(time), + "output": null, + "build_status": true + }); + + assert_eq!(correct_json, serde_json::to_value(&build).unwrap()); + + build.output = Some("some random stuff".to_string()); + let correct_json = json!({ + "id": 22, + "rustc_version": "rustc 1.43.0 (4fb7144ed 2020-04-20)", + "cratesfyi_version": "docsrs 0.6.0 (3dd32ec 2020-05-01)", + "build_time": time.format("%+").to_string(), + "build_time_relative": duration_to_str(time), + "output": "some random stuff", + "build_status": true + }); + + assert_eq!(correct_json, serde_json::to_value(&build).unwrap()); + } + + #[test] + fn serialize_build_page() { + let time = Utc::now(); + let build = Build { + id: 22, + rustc_version: "rustc 1.43.0 (4fb7144ed 2020-04-20)".to_string(), + cratesfyi_version: "docsrs 0.6.0 (3dd32ec 2020-05-01)".to_string(), + build_status: true, + build_time: time, + output: None, + }; + let limits = Limits::default(); + let mut builds = BuildsPage { + metadata: Some(MetaData { + name: "serde".to_string(), + version: "1.0.0".to_string(), + description: Some("serde does stuff".to_string()), + target_name: None, + rustdoc_status: true, + default_target: "x86_64-unknown-linux-gnu".to_string(), + }), + builds: vec![build.clone()], + build_details: Some(build.clone()), + limits: limits.clone(), + }; + + let correct_json = json!({ + "metadata": { + "name": "serde", + "version": "1.0.0", + "description": "serde does stuff", + "target_name": null, + "rustdoc_status": true, + "default_target": "x86_64-unknown-linux-gnu" + }, + "builds": [{ + "id": 22, + "rustc_version": "rustc 1.43.0 (4fb7144ed 2020-04-20)", + "cratesfyi_version": "docsrs 0.6.0 (3dd32ec 2020-05-01)", + "build_time": time.format("%+").to_string(), + "build_time_relative": duration_to_str(time), + "output": null, + "build_status": true + }], + "build_details": { + "id": 22, + "rustc_version": "rustc 1.43.0 (4fb7144ed 2020-04-20)", + "cratesfyi_version": "docsrs 0.6.0 (3dd32ec 2020-05-01)", + "build_time": time.format("%+").to_string(), + "build_time_relative": duration_to_str(time), + "output": null, + "build_status": true + }, + "limits": limits.for_website(), + }); + + assert_eq!(correct_json, serde_json::to_value(&builds).unwrap()); + + builds.metadata = None; + let correct_json = json!({ + "metadata": null, + "builds": [{ + "id": 22, + "rustc_version": "rustc 1.43.0 (4fb7144ed 2020-04-20)", + "cratesfyi_version": "docsrs 0.6.0 (3dd32ec 2020-05-01)", + "build_time": time.format("%+").to_string(), + "build_time_relative": duration_to_str(time), + "output": null, + "build_status": true + }], + "build_details": { + "id": 22, + "rustc_version": "rustc 1.43.0 (4fb7144ed 2020-04-20)", + "cratesfyi_version": "docsrs 0.6.0 (3dd32ec 2020-05-01)", + "build_time": time.format("%+").to_string(), + "build_time_relative": duration_to_str(time), + "output": null, + "build_status": true + }, + "limits": limits.for_website(), + }); + + assert_eq!(correct_json, serde_json::to_value(&builds).unwrap()); + + builds.builds = Vec::new(); + let correct_json = json!({ + "metadata": null, + "builds": [], + "build_details": { + "id": 22, + "rustc_version": "rustc 1.43.0 (4fb7144ed 2020-04-20)", + "cratesfyi_version": "docsrs 0.6.0 (3dd32ec 2020-05-01)", + "build_time": time.format("%+").to_string(), + "build_time_relative": duration_to_str(time), + "output": null, + "build_status": true + }, + "limits": limits.for_website() + }); + + assert_eq!(correct_json, serde_json::to_value(&builds).unwrap()); + + builds.build_details = None; + let correct_json = json!({ + "metadata": null, + "builds": [], + "build_details": null, + "limits": limits.for_website(), + }); + + assert_eq!(correct_json, serde_json::to_value(&builds).unwrap()); } } diff --git a/src/web/crate_details.rs b/src/web/crate_details.rs index 47bb23ba7..4e5ff15a9 100644 --- a/src/web/crate_details.rs +++ b/src/web/crate_details.rs @@ -1,17 +1,23 @@ use super::error::Nope; -use super::{match_version, redirect_base, render_markdown, MatchSemver, MetaData}; -use crate::{db::Pool, impl_webpage, web::page::WebPage}; +use super::page::Page; +use super::{ + duration_to_str, match_version, redirect_base, render_markdown, MatchSemver, MetaData, +}; +use crate::db::Pool; use chrono::{DateTime, NaiveDateTime, Utc}; use iron::prelude::*; use iron::{status, Url}; use postgres::Connection; use router::Router; -use serde::{ser::Serializer, Serialize}; +use serde::{ + ser::{SerializeStruct, Serializer}, + Serialize, +}; use serde_json::Value; // TODO: Add target name and versions -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq)] pub struct CrateDetails { name: String, version: String, @@ -20,9 +26,7 @@ pub struct CrateDetails { owners: Vec<(String, String)>, authors_json: Option, dependencies: Option, - #[serde(serialize_with = "optional_markdown")] readme: Option, - #[serde(serialize_with = "optional_markdown")] rustdoc: Option, // this is description_long in database release_time: DateTime, build_status: bool, @@ -46,16 +50,60 @@ pub struct CrateDetails { documentation_url: Option, } -fn optional_markdown(markdown: &Option, serializer: S) -> Result -where - S: Serializer, -{ - if let Some(ref markdown) = markdown { - Some(render_markdown(&markdown)) - } else { - None +impl Serialize for CrateDetails { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Make sure that the length parameter passed to serde is correct by + // adding the someness of `readme` and `rustdoc` to the total. `true` + // is 1 and `false` is 0, so it increments if the value is some (and therefore + // needs to be serialized) + let mut state = serializer.serialize_struct( + "CrateDetails", + 26 + self.readme.is_some() as usize + self.rustdoc.is_some() as usize, + )?; + + state.serialize_field("metadata", &self.metadata)?; + state.serialize_field("name", &self.name)?; + state.serialize_field("version", &self.version)?; + state.serialize_field("description", &self.description)?; + state.serialize_field("authors", &self.authors)?; + state.serialize_field("owners", &self.owners)?; + state.serialize_field("authors_json", &self.authors_json)?; + state.serialize_field("dependencies", &self.dependencies)?; + + if let Some(ref readme) = self.readme { + state.serialize_field("readme", &render_markdown(&readme))?; + } + + if let Some(ref rustdoc) = self.rustdoc { + state.serialize_field("rustdoc", &render_markdown(&rustdoc))?; + } + + state.serialize_field("release_time", &duration_to_str(self.release_time))?; + state.serialize_field("build_status", &self.build_status)?; + state.serialize_field("last_successful_build", &self.last_successful_build)?; + state.serialize_field("rustdoc_status", &self.rustdoc_status)?; + state.serialize_field("repository_url", &self.repository_url)?; + state.serialize_field("homepage_url", &self.homepage_url)?; + state.serialize_field("keywords", &self.keywords)?; + state.serialize_field("have_examples", &self.have_examples)?; + state.serialize_field("target_name", &self.target_name)?; + state.serialize_field("releases", &self.releases)?; + state.serialize_field("github", &self.github)?; + state.serialize_field("github_stars", &self.github_stars)?; + state.serialize_field("github_forks", &self.github_forks)?; + state.serialize_field("github_issues", &self.github_issues)?; + state.serialize_field("metadata", &self.metadata)?; + state.serialize_field("is_library", &self.is_library)?; + state.serialize_field("doc_targets", &self.doc_targets)?; + state.serialize_field("yanked", &self.yanked)?; + state.serialize_field("license", &self.license)?; + state.serialize_field("documentation_url", &self.documentation_url)?; + + state.end() } - .serialize(serializer) } #[derive(Debug, Clone, Eq, PartialEq, Serialize)] @@ -238,6 +286,48 @@ impl CrateDetails { .find(|release| !release.yanked) .unwrap_or(&self.releases[0]) } + + #[cfg(test)] + pub fn default_tester(release_time: DateTime) -> Self { + Self { + name: "rcc".to_string(), + version: "100.0.0".to_string(), + description: None, + authors: vec![], + owners: vec![], + authors_json: None, + dependencies: None, + readme: None, + rustdoc: None, + release_time, + build_status: true, + last_successful_build: None, + rustdoc_status: true, + repository_url: None, + homepage_url: None, + keywords: None, + yanked: false, + have_examples: true, + target_name: "x86_64-unknown-linux-gnu".to_string(), + releases: vec![], + github: true, + github_stars: None, + github_forks: None, + github_issues: None, + metadata: MetaData { + name: "serde".to_string(), + version: "1.0.0".to_string(), + description: Some("serde does stuff".to_string()), + target_name: None, + rustdoc_status: true, + default_target: "x86_64-unknown-linux-gnu".to_string(), + }, + is_library: true, + doc_targets: vec![], + license: None, + documentation_url: None, + } + } } fn map_to_release(conn: &Connection, crate_id: i32, version: String) -> Release { @@ -263,19 +353,10 @@ fn map_to_release(conn: &Connection, crate_id: i32, version: String) -> Release } } -#[derive(Debug, Clone, PartialEq, Serialize)] -struct CrateDetailsPage { - details: Option, -} - -impl_webpage! { - CrateDetailsPage = "crate/details.html", -} - pub fn crate_details_handler(req: &mut Request) -> IronResult { let router = extension!(req, Router); // this handler must always called with a crate name - let name = cexpect!(req, router.find("name")); + let name = cexpect!(router.find("name")); let req_version = router.find("version"); let conn = extension!(req, Pool).get()?; @@ -284,23 +365,19 @@ pub fn crate_details_handler(req: &mut Request) -> IronResult { Some(MatchSemver::Exact((version, _))) => { let details = CrateDetails::new(&conn, &name, &version); - CrateDetailsPage { details }.into_response(req) + Page::new(details) + .set_true("show_package_navigation") + .set_true("javascript_highlightjs") + .set_true("package_navigation_crate_tab") + .to_resp("crate_details") } - Some(MatchSemver::Semver((version, _))) => { - let url = ctry!( - req, - Url::parse(&format!( - "{}/crate/{}/{}", - redirect_base(req), - name, - version - )), - ); + let url = ctry!(Url::parse( + &format!("{}/crate/{}/{}", redirect_base(req), name, version)[..] + )); Ok(super::redirect(url)) } - None => Err(IronError::new(Nope::CrateNotFound, status::NotFound)), } } @@ -309,7 +386,9 @@ pub fn crate_details_handler(req: &mut Request) -> IronResult { mod tests { use super::*; use crate::test::TestDatabase; + use chrono::Utc; use failure::Error; + use serde_json::json; fn assert_last_successful_build_equals( db: &TestDatabase, @@ -554,4 +633,82 @@ mod tests { Ok(()) }) } + + #[test] + fn serialize_crate_details() { + let time = Utc::now(); + let mut details = CrateDetails::default_tester(time); + + let mut correct_json = json!({ + "name": "rcc", + "version": "100.0.0", + "description": null, + "authors": [], + "owners": [], + "authors_json": null, + "dependencies": null, + "release_time": super::super::duration_to_str(time), + "build_status": true, + "last_successful_build": null, + "rustdoc_status": true, + "repository_url": null, + "homepage_url": null, + "keywords": null, + "have_examples": true, + "target_name": "x86_64-unknown-linux-gnu", + "releases": [], + "github": true, + "yanked": false, + "github_stars": null, + "github_forks": null, + "github_issues": null, + "metadata": { + "name": "serde", + "version": "1.0.0", + "description": "serde does stuff", + "target_name": null, + "rustdoc_status": true, + "default_target": "x86_64-unknown-linux-gnu" + }, + "is_library": true, + "doc_targets": [], + "license": null, + "documentation_url": null + }); + + assert_eq!(correct_json, serde_json::to_value(&details).unwrap()); + + let authors = vec![("Somebody".to_string(), "somebody@somebody.com".to_string())]; + let owners = vec![("Owner".to_string(), "owner@ownsstuff.com".to_string())]; + let description = "serde does stuff".to_string(); + + correct_json["description"] = Value::String(description.clone()); + correct_json["owners"] = serde_json::to_value(&owners).unwrap(); + correct_json["authors_json"] = serde_json::to_value(&authors).unwrap(); + correct_json["authors"] = serde_json::to_value(&authors).unwrap(); + + details.description = Some(description); + details.owners = owners; + details.authors_json = Some(serde_json::to_value(&authors).unwrap()); + details.authors = authors; + + assert_eq!(correct_json, serde_json::to_value(&details).unwrap()); + } + + #[test] + fn serialize_releases() { + let release = Release { + version: "idkman".to_string(), + build_status: true, + yanked: true, + }; + + let correct_json = json!({ + "version": "idkman", + "build_status": true, + "yanked": true, + }); + + assert_eq!(correct_json, serde_json::to_value(&release).unwrap()); + } } diff --git a/src/web/error.rs b/src/web/error.rs index 0abcc3e92..2c9060c36 100644 --- a/src/web/error.rs +++ b/src/web/error.rs @@ -1,9 +1,12 @@ use crate::{ db::PoolError, - web::{page::WebPage, releases::Search, ErrorPage}, + web::{ + page::{Page, WebPage}, + releases::Search, + }, }; use failure::Fail; -use iron::{status::Status, Handler, IronError, IronResult, Plugin, Request, Response}; +use iron::{status, Handler, IronError, IronResult, Plugin, Request, Response}; use params::{Params, Value}; use std::{error::Error, fmt}; @@ -33,24 +36,18 @@ impl Handler for Nope { match *self { Nope::ResourceNotFound => { // user tried to navigate to a resource (doc page/file) that doesn't exist - // TODO: Display the attempted page - ErrorPage { - title: "The requested resource does not exist", - message: Some("no such resource".into()), - status: Status::NotFound, - } - .into_response(req) + 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 - // TODO: Display the attempted crate and a link to a search for said crate - ErrorPage { - title: "The requested crate does not exist", - message: Some("no such crate".into()), - status: Status::NotFound, - } - .into_response(req) + Page::new("no such crate".to_owned()) + .set_status(status::NotFound) + .title("The requested crate does not exist") + .to_resp("error") } Nope::NoResults => { @@ -61,7 +58,7 @@ impl Handler for Nope { Search { title: format!("No crates found matching '{}'", query), search_query: Some(query.to_owned()), - status: Status::NotFound, + status: status::NotFound, ..Default::default() } .into_response(req) @@ -69,7 +66,7 @@ impl Handler for Nope { // user did a search with no search terms Search { title: "No results given for empty search query".to_owned(), - status: Status::NotFound, + status: status::NotFound, ..Default::default() } .into_response(req) @@ -78,12 +75,10 @@ impl Handler for Nope { Nope::InternalServerError => { // something went wrong, details should have been logged - ErrorPage { - title: "Internal server error", - message: Some("internal server error".into()), - status: Status::InternalServerError, - } - .into_response(req) + Page::new("internal server error".to_owned()) + .set_status(status::InternalServerError) + .title("Internal server error") + .to_resp("error") } } } @@ -91,6 +86,6 @@ impl Handler for Nope { impl From for IronError { fn from(err: PoolError) -> IronError { - IronError::new(err.compat(), Status::InternalServerError) + IronError::new(err.compat(), status::InternalServerError) } } diff --git a/src/web/metrics.rs b/src/web/metrics.rs index e0eeeaf37..dd891c726 100644 --- a/src/web/metrics.rs +++ b/src/web/metrics.rs @@ -154,9 +154,9 @@ pub fn metrics_handler(req: &mut Request) -> IronResult { USED_DB_CONNECTIONS.set(pool.used_connections() as i64); IDLE_DB_CONNECTIONS.set(pool.idle_connections() as i64); - QUEUED_CRATES_COUNT.set(ctry!(req, queue.pending_count()) as i64); - PRIORITIZED_CRATES_COUNT.set(ctry!(req, queue.prioritized_count()) as i64); - FAILED_CRATES_COUNT.set(ctry!(req, queue.failed_count()) as i64); + QUEUED_CRATES_COUNT.set(ctry!(queue.pending_count()) as i64); + PRIORITIZED_CRATES_COUNT.set(ctry!(queue.prioritized_count()) as i64); + FAILED_CRATES_COUNT.set(ctry!(queue.failed_count()) as i64); #[cfg(target_os = "linux")] { @@ -169,12 +169,12 @@ pub fn metrics_handler(req: &mut Request) -> IronResult { let mut buffer = Vec::new(); let families = prometheus::gather(); - ctry!(req, TextEncoder::new().encode(&families, &mut buffer)); + ctry!(TextEncoder::new().encode(&families, &mut buffer)); let mut resp = Response::with(buffer); resp.status = Some(Status::Ok); - resp.headers.set(ContentType::plaintext()); - + resp.headers + .set(ContentType("text/plain; version=0.0.4".parse().unwrap())); Ok(resp) } diff --git a/src/web/mod.rs b/src/web/mod.rs index d74d5a92c..cc12c5365 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -7,23 +7,15 @@ use log::{debug, info}; /// ctry! (cratesfyitry) is extremely similar to try! and itry! /// except it returns an error page response instead of plain Err. macro_rules! ctry { - ($req:expr, $result:expr $(,)?) => { + ($result:expr) => { match $result { - Ok(success) => success, - Err(error) => { - ::log::error!("{}\n{:?}", error, ::backtrace::Backtrace::new()); - - // This is very ugly, but it makes it impossible to get a type inference error - // from this macro - let error = $crate::web::ErrorPage { - title: "Internal Server Error", - message: ::std::option::Option::Some(::std::borrow::Cow::Owned( - ::std::format!("{}", error), - )), - status: ::iron::status::BadRequest, - }; - - return $crate::web::page::WebPage::into_response(error, $req); + Ok(v) => v, + Err(e) => { + log::error!("{}\n{:?}", e, backtrace::Backtrace::new()); + return $crate::web::page::Page::new(format!("{}", e)) + .title("Internal Server Error") + .set_status(::iron::status::BadRequest) + .to_resp("error"); } } }; @@ -32,24 +24,18 @@ macro_rules! ctry { /// cexpect will check an option and if it's not Some /// it will return an error page response macro_rules! cexpect { - ($req:expr, $option:expr $(,)?) => { + ($option:expr) => { match $option { - Some(success) => success, + Some(v) => v, None => { - ::log::error!( + log::error!( "called cexpect!() on a `None` value\n{:?}", - ::backtrace::Backtrace::new(), + backtrace::Backtrace::new() ); - - // This is very ugly, but it makes it impossible to get a type inference error - // from this macro - let error = $crate::web::ErrorPage { - title: "Internal Server Error", - message: None, - status: ::iron::status::BadRequest, - }; - - return $crate::web::page::WebPage::into_response(error, $req); + return $crate::web::page::Page::new("Internal Server Error".to_owned()) + .title("Internal Server Error") + .set_status(::iron::status::BadRequest) + .to_resp("error"); } } }; @@ -57,12 +43,9 @@ macro_rules! cexpect { /// Gets an extension from Request macro_rules! extension { - ($req:expr, $ext:ty) => {{ - // Bind $req so we can have good type errors and avoid re-evaluation - let request: &::iron::Request = $req; - - cexpect!(request, request.extensions.get::<$ext>()) - }}; + ($req:expr, $ext:ty) => { + cexpect!($req.extensions.get::<$ext>()) + }; } mod builds; @@ -77,25 +60,23 @@ mod rustdoc; mod sitemap; mod source; -use crate::{config::Config, db::Pool, impl_webpage, BuildQueue}; +use self::extensions::InjectExtensions; +use self::page::TemplateData; +use crate::config::Config; +use crate::db::Pool; +use crate::BuildQueue; use chrono::{DateTime, Utc}; -use extensions::InjectExtensions; use failure::Error; -use iron::{ - self, - headers::{CacheControl, CacheDirective, ContentType, Expires, HttpDate}, - modifiers::Redirect, - status, - status::Status, - Chain, Handler, Iron, IronError, IronResult, Listening, Request, Response, Url, -}; -use page::TemplateData; +use handlebars_iron::{DirectorySource, HandlebarsEngine, SourceError}; +use iron::headers::{CacheControl, CacheDirective, ContentType, Expires, HttpDate}; +use iron::modifiers::Redirect; +use iron::prelude::*; +use iron::{self, status, Handler, Listening, Url}; use postgres::Connection; use router::NoRoute; use semver::{Version, VersionReq}; -use serde::Serialize; use staticfile::Static; -use std::{borrow::Cow, env, fmt, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration}; +use std::{env, fmt, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration}; /// Duration of static files for staticfile and DatabaseFileHandler (in seconds) const STATIC_FILE_CACHE_DURATION: u64 = 60 * 60 * 24 * 30 * 12; // 12 months @@ -106,6 +87,17 @@ const OPENSEARCH_XML: &[u8] = include_bytes!("opensearch.xml"); const DEFAULT_BIND: &str = "0.0.0.0:3000"; +fn handlebars_engine() -> Result { + // TODO: Use DocBuilderOptions for paths + let mut hbse = HandlebarsEngine::new(); + hbse.add(Box::new(DirectorySource::new("./templates", ".hbs"))); + + // load templates + hbse.reload()?; + + Ok(hbse) +} + struct CratesfyiHandler { shared_resource_handler: Box, router_handler: Box, @@ -116,9 +108,11 @@ struct CratesfyiHandler { impl CratesfyiHandler { fn chain(inject_extensions: InjectExtensions, base: H) -> Chain { + let hbse = handlebars_engine().expect("Failed to load handlebar templates"); + let mut chain = Chain::new(base); chain.link_before(inject_extensions); - + chain.link_after(hbse); chain } @@ -563,17 +557,16 @@ fn ico_handler(req: &mut Request) -> IronResult { } else { // if we're looking for something like "favicon-20190317-1.35.0-nightly-c82834e2b.ico", // redirect to the plain one so that the above branch can trigger with the correct filename - let url = ctry!( - req, - Url::parse(&format!("{}/favicon.ico", redirect_base(req))), - ); + let url = ctry!(Url::parse( + &format!("{}/favicon.ico", redirect_base(req))[..] + )); Ok(redirect(url)) } } /// MetaData used in header -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] pub(crate) struct MetaData { pub(crate) name: String, pub(crate) version: String, @@ -613,25 +606,13 @@ impl MetaData { } } -#[derive(Debug, Clone, PartialEq, Serialize)] -pub(crate) struct ErrorPage { - /// The title of the page - pub title: &'static str, - /// The error message, displayed as a description - pub message: Option>, - #[serde(skip)] - pub status: Status, -} - -impl_webpage! { - ErrorPage = "error.html", - status = |err| err.status, -} - #[cfg(test)] mod test { use super::*; - use crate::{test::*, web::match_version}; + use crate::{ + test::*, + web::{handlebars_engine, match_version}, + }; use kuchiki::traits::TendrilSink; use serde_json::json; @@ -840,6 +821,11 @@ mod test { }); } + #[test] + fn test_templates_are_valid() { + handlebars_engine().expect("Failed to load handlebar templates"); + } + #[test] fn serialize_metadata() { let mut metadata = MetaData { diff --git a/src/web/page/handlebars.rs b/src/web/page/handlebars.rs new file mode 100644 index 000000000..c493a8f8e --- /dev/null +++ b/src/web/page/handlebars.rs @@ -0,0 +1,196 @@ +//! Generic page struct + +use handlebars_iron::Template; +use iron::response::Response; +use iron::{status, IronResult, Set}; +use once_cell::sync::Lazy; +use serde::{ + ser::{SerializeStruct, Serializer}, + Serialize, +}; +use serde_json::Value; +use std::collections::BTreeMap; + +static RUSTC_RESOURCE_SUFFIX: Lazy = + Lazy::new(|| load_rustc_resource_suffix().unwrap_or_else(|_| "???".into())); + +fn load_rustc_resource_suffix() -> Result { + // New instances of the configuration or the connection pool shouldn't be created inside the + // application, but we're removing handlebars so this is not going to be a problem in the long + // term. To avoid wasting resources, the pool is hardcoded to only keep one connection alive. + let mut config = crate::Config::from_env()?; + config.max_pool_size = 1; + config.min_pool_idle = 1; + let pool = crate::db::Pool::new(&config)?; + let conn = pool.get()?; + + let res = conn.query( + "SELECT value FROM config WHERE name = 'rustc_version';", + &[], + )?; + if res.is_empty() { + failure::bail!("missing rustc version"); + } + + if let Some(Ok(vers)) = res.get(0).get_opt::<_, Value>("value") { + if let Some(vers_str) = vers.as_str() { + return Ok(crate::utils::parse_rustc_version(vers_str)?); + } + } + + failure::bail!("failed to parse the rustc version"); +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Page { + title: Option, + content: T, + status: status::Status, + varss: BTreeMap, + varsb: BTreeMap, + varsi: BTreeMap, + rustc_resource_suffix: &'static str, +} + +impl Page { + pub fn new(content: T) -> Page { + Page { + title: None, + content, + status: status::Ok, + varss: BTreeMap::new(), + varsb: BTreeMap::new(), + varsi: BTreeMap::new(), + rustc_resource_suffix: &RUSTC_RESOURCE_SUFFIX, + } + } + + /// 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 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 + } + + #[allow(clippy::wrong_self_convention)] + 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 Serialize for Page { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Make sure that the length parameter passed to serde is correct by + // adding the someness of the global alert to the total. `true` + // is 1 and `false` is 0, so it increments if the value is some (and therefore + // needs to be serialized) + let mut state = serializer.serialize_struct( + "Page", + 8 + crate::GLOBAL_ALERT.is_some() as usize + self.title.is_some() as usize, + )?; + + if let Some(ref title) = self.title { + state.serialize_field("title", title)?; + } + + state.serialize_field("has_global_alert", &crate::GLOBAL_ALERT.is_some())?; + if let Some(ref global_alert) = crate::GLOBAL_ALERT { + state.serialize_field("global_alert", global_alert)?; + } + + state.serialize_field("content", &self.content)?; + state.serialize_field("rustc_resource_suffix", self.rustc_resource_suffix)?; + state.serialize_field("cratesfyi_version", crate::BUILD_VERSION)?; + state.serialize_field( + "cratesfyi_version_safe", + &build_version_safe(crate::BUILD_VERSION), + )?; + state.serialize_field("varss", &self.varss)?; + state.serialize_field("varsb", &self.varsb)?; + state.serialize_field("varsi", &self.varsi)?; + + state.end() + } +} + +fn build_version_safe(version: &str) -> String { + version.replace(" ", "-").replace("(", "").replace(")", "") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::web::releases; + use iron::Url; + + #[test] + fn load_page_from_releases() { + crate::test::wrapper(|env| { + let db = env.db(); + db.fake_release().name("foo").version("0.1.0").create()?; + let packages = releases::get_releases(&db.conn(), 1, 1, releases::Order::ReleaseTime); + + let mut varsb = BTreeMap::new(); + varsb.insert("show_search_form".into(), true); + varsb.insert("hide_package_navigation".into(), true); + + let correct_page = Page { + title: None, + content: packages.clone(), + status: status::Status::Ok, + varss: BTreeMap::new(), + varsb, + varsi: BTreeMap::new(), + rustc_resource_suffix: &RUSTC_RESOURCE_SUFFIX, + }; + + let page = Page::new(packages) + .set_true("show_search_form") + .set_true("hide_package_navigation"); + + assert_eq!(page, correct_page); + + Ok(()) + }) + } + + #[test] + fn build_version_url_safe() { + let safe = format!( + "https://docs.rs/builds/{}", + build_version_safe(crate::BUILD_VERSION) + ); + assert!(Url::parse(&safe).is_ok()); + } +} diff --git a/src/web/page/mod.rs b/src/web/page/mod.rs index b8a42458e..c8c1ad175 100644 --- a/src/web/page/mod.rs +++ b/src/web/page/mod.rs @@ -1,6 +1,8 @@ +mod handlebars; mod templates; mod web_page; +pub use handlebars::*; pub(crate) use templates::TemplateData; pub(crate) use web_page::WebPage; diff --git a/src/web/page/templates.rs b/src/web/page/templates.rs index 8d02af143..801de6c29 100644 --- a/src/web/page/templates.rs +++ b/src/web/page/templates.rs @@ -16,7 +16,7 @@ use std::{ use tera::{Result as TeraResult, Tera}; use walkdir::WalkDir; -const TEMPLATES_DIRECTORY: &str = "templates"; +const TEMPLATES_DIRECTORY: &str = "tera-templates"; /// Holds all data relevant to templating #[derive(Debug)] @@ -45,7 +45,7 @@ impl TemplateData { let mut watcher = watcher(tx, Duration::from_secs(2)).unwrap(); watcher - .watch(TEMPLATES_DIRECTORY, RecursiveMode::Recursive) + .watch("tera-templates", RecursiveMode::Recursive) .unwrap(); thread::spawn(move || { diff --git a/src/web/releases.rs b/src/web/releases.rs index 0bad8244e..33f25fa46 100644 --- a/src/web/releases.rs +++ b/src/web/releases.rs @@ -1,10 +1,14 @@ //! Releases web handlers use crate::{ - build_queue::QueuedCrate, db::Pool, impl_webpage, - web::{error::Nope, match_version, page::WebPage, redirect_base}, + web::{ + error::Nope, + match_version, + page::{Page, WebPage}, + redirect_base, + }, BuildQueue, }; use chrono::{DateTime, NaiveDateTime, Utc}; @@ -452,10 +456,10 @@ pub fn author_handler(req: &mut Request) -> IronResult { .find("page") .and_then(|page_num| page_num.parse().ok()) .unwrap_or(1); - let author = router + let author = ctry!(router .find("author") // TODO: Accurate error here, the author wasn't provided - .ok_or_else(|| IronError::new(Nope::CrateNotFound, status::NotFound))?; + .ok_or_else(|| IronError::new(Nope::CrateNotFound, status::NotFound))); let (author_name, releases) = { let conn = extension!(req, Pool).get()?; @@ -468,7 +472,7 @@ pub fn author_handler(req: &mut Request) -> IronResult { page_number, RELEASES_IN_RELEASES, // TODO: Is this fallible? - cexpect!(req, author.nth(1)), + cexpect!(author.nth(1)), ) } else { get_releases_by_author(&conn, page_number, RELEASES_IN_RELEASES, author) @@ -536,7 +540,7 @@ impl_webpage! { pub fn search_handler(req: &mut Request) -> IronResult { use params::{Params, Value}; - let params = ctry!(req, req.get::()); + let params = ctry!(req.get::()); let query = params.find(&["query"]); let conn = extension!(req, Pool).get()?; @@ -550,10 +554,8 @@ pub fn search_handler(req: &mut Request) -> IronResult { // FIXME: This is a fast query but using a constant // There are currently 280 crates with docs and 100+ // starts. This should be fine for a while. - let rows = ctry!( - req, - conn.query( - "SELECT crates.name, + let rows = ctry!(conn.query( + "SELECT crates.name, releases.version, releases.target_name FROM crates @@ -561,24 +563,20 @@ pub fn search_handler(req: &mut Request) -> IronResult { ON crates.latest_version_id = releases.id WHERE github_stars >= 100 AND rustdoc_status = true OFFSET FLOOR(RANDOM() * 280) LIMIT 1", - &[] - ), - ); + &[] + )); let row = rows.into_iter().next().unwrap(); let name: String = row.get("name"); let version: String = row.get("version"); let target_name: String = row.get("target_name"); - let url = ctry!( - req, - Url::parse(&format!( - "{}/{}/{}/{}", - redirect_base(req), - name, - version, - target_name - )), - ); + let url = ctry!(Url::parse(&format!( + "{}/{}/{}/{}", + redirect_base(req), + name, + version, + target_name + ))); let mut resp = Response::with((status::Found, Redirect(url))); resp.headers.set(Expires(HttpDate(time::now()))); @@ -597,15 +595,12 @@ pub fn search_handler(req: &mut Request) -> IronResult { // match_version should handle this instead of this code block. // This block is introduced to fix #163 let rustdoc_status = { - let rows = ctry!( - req, - conn.query( - "SELECT rustdoc_status + let rows = ctry!(conn.query( + "SELECT rustdoc_status FROM releases WHERE releases.id = $1", - &[&id] - ), - ); + &[&id] + )); rows.into_iter() .next() @@ -614,20 +609,19 @@ pub fn search_handler(req: &mut Request) -> IronResult { }; let url = if rustdoc_status { - ctry!( - req, - Url::parse(&format!("{}/{}/{}", redirect_base(req), query, version)), - ) + ctry!(Url::parse(&format!( + "{}/{}/{}", + redirect_base(req), + query, + version, + ))) } else { - ctry!( - req, - Url::parse(&format!( - "{}/crate/{}/{}", - redirect_base(req), - query, - version, - )), - ) + ctry!(Url::parse(&format!( + "{}/crate/{}/{}", + redirect_base(req), + query, + version, + ))) }; let mut resp = Response::with((status::Found, Redirect(url))); @@ -657,60 +651,44 @@ pub fn search_handler(req: &mut Request) -> IronResult { } } -#[derive(Debug, Clone, PartialEq, Serialize)] -struct ReleaseActivity { - description: &'static str, - activity_data: Value, -} - -impl_webpage! { - ReleaseActivity = "releases/activity.html", -} - pub fn activity_handler(req: &mut Request) -> IronResult { let conn = extension!(req, Pool).get()?; - let activity_data: Value = ctry!( - req, - conn.query( - "SELECT value FROM config WHERE name = 'release_activity'", - &[] - ), - ) + let release_activity_data: Value = ctry!(conn.query( + "SELECT value FROM config WHERE name = 'release_activity'", + &[] + )) .iter() .next() .map_or(Value::Null, |row| row.get("value")); - ReleaseActivity { - description: "Monthly release activity", - activity_data, - } - .into_response(req) -} - -#[derive(Debug, Clone, PartialEq, Serialize)] -struct BuildQueuePage { - description: &'static str, - queue: Vec, -} - -impl_webpage! { - BuildQueuePage = "releases/build_queue.html", + 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") } pub fn build_queue_handler(req: &mut Request) -> IronResult { - let mut queue = ctry!(req, extension!(req, BuildQueue).queued_crates()); - for krate in queue.iter_mut() { + let queue = extension!(req, BuildQueue); + + let mut crates = ctry!(queue.queued_crates()); + for krate in &mut crates { // The priority here is inverted: in the database if a crate has a higher priority it // will be built after everything else, which is counter-intuitive for people not // familiar with docs.rs's inner workings. krate.priority = -krate.priority; } - BuildQueuePage { - description: "List of crates scheduled to build", - queue, - } - .into_response(req) + let is_empty = crates.is_empty(); + Page::new(crates) + .title("Build queue") + .set("description", "List of crates scheduled to build") + .set_bool("queue_empty", is_empty) + .set_true("show_releases_navigation") + .set_true("releases_queue_tab") + .to_resp("releases_queue") } #[cfg(test)] diff --git a/src/web/rustdoc.rs b/src/web/rustdoc.rs index ef982df3d..a5dc242d0 100644 --- a/src/web/rustdoc.rs +++ b/src/web/rustdoc.rs @@ -1,22 +1,55 @@ //! rustdoc handler -use crate::{ - db::Pool, - impl_webpage, utils, - web::{ - crate_details::CrateDetails, error::Nope, file::File, match_version, metrics, - page::WebPage, redirect_base, MatchSemver, - }, - Config, -}; -use iron::{ - headers::{CacheControl, CacheDirective, Expires, HttpDate}, - modifiers::Redirect, - status, Handler, IronError, IronResult, Plugin, Request, Response, Url, -}; +use super::crate_details::CrateDetails; +use super::error::Nope; +use super::file::File; +use super::metrics; +use super::page::Page; +use super::redirect_base; +use super::{match_version, MatchSemver}; +use crate::db::Pool; +use crate::utils; +use crate::Config; +use iron::headers::{CacheControl, CacheDirective, Expires, HttpDate}; +use iron::modifiers::Redirect; +use iron::prelude::*; +use iron::Handler; +use iron::{status, Url}; use postgres::Connection; use router::Router; -use serde::Serialize; +use serde::ser::{Serialize, SerializeStruct, Serializer}; + +#[derive(Debug, Default)] +struct RustdocPage { + head: String, + body: String, + body_class: String, + name: String, + full: String, + version: String, + description: Option, + crate_details: Option, +} + +impl Serialize for RustdocPage { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("RustdocPage", 9)?; + state.serialize_field("rustdoc_head", &self.head)?; + state.serialize_field("rustdoc_body", &self.body)?; + state.serialize_field("rustdoc_body_class", &self.body_class)?; + state.serialize_field("rustdoc_full", &self.full)?; + state.serialize_field("rustdoc_status", &true)?; + state.serialize_field("name", &self.name)?; + state.serialize_field("version", &self.version)?; + state.serialize_field("description", &self.description)?; + state.serialize_field("crate_details", &self.crate_details)?; + + state.end() + } +} #[derive(Clone)] pub struct RustLangRedirector { @@ -30,7 +63,6 @@ impl RustLangRedirector { .join(target) .expect("failed to append crate name to rust-lang.org base URL"); let url = Url::from_generic_url(url).expect("failed to convert url::Url to iron::Url"); - Self { url } } } @@ -69,7 +101,7 @@ pub fn rustdoc_redirector_handler(req: &mut Request) -> IronResult { url_str.push('?'); url_str.push_str(query); } - let url = ctry!(req, Url::parse(&url_str)); + let url = ctry!(Url::parse(&url_str[..])); let mut resp = Response::with((status::Found, Redirect(url))); resp.headers.set(Expires(HttpDate(time::now()))); @@ -77,10 +109,9 @@ pub fn rustdoc_redirector_handler(req: &mut Request) -> IronResult { } fn redirect_to_crate(req: &Request, name: &str, vers: &str) -> IronResult { - let url = ctry!( - req, - Url::parse(&format!("{}/crate/{}/{}", redirect_base(req), name, vers)), - ); + let url = ctry!(Url::parse( + &format!("{}/crate/{}/{}", redirect_base(req), name, vers)[..] + )); let mut resp = Response::with((status::Found, Redirect(url))); resp.headers.set(Expires(HttpDate(time::now()))); @@ -132,7 +163,7 @@ pub fn rustdoc_redirector_handler(req: &mut Request) -> IronResult { let conn = extension!(req, Pool).get()?; // this handler should never called without crate pattern - let crate_name = cexpect!(req, router.find("crate")); + let crate_name = cexpect!(router.find("crate")); let mut crate_name = percent_decode(crate_name.as_bytes()) .decode_utf8() .unwrap_or_else(|_| crate_name.into()) @@ -159,15 +190,12 @@ pub fn rustdoc_redirector_handler(req: &mut Request) -> IronResult { // get target name and whether it has docs // FIXME: This is a bit inefficient but allowing us to use less code in general let (target_name, has_docs): (String, bool) = { - let rows = ctry!( - req, - conn.query( - "SELECT target_name, rustdoc_status - FROM releases - WHERE releases.id = $1", - &[&id] - ), - ); + let rows = ctry!(conn.query( + "SELECT target_name, rustdoc_status + FROM releases + WHERE releases.id = $1", + &[&id] + )); (rows.get(0).get(0), rows.get(0).get(1)) }; @@ -183,22 +211,6 @@ pub fn rustdoc_redirector_handler(req: &mut Request) -> IronResult { } } -#[derive(Debug, Clone, PartialEq, Serialize)] -struct RustdocPage { - latest_path: String, - latest_version: String, - inner_path: String, - is_latest_version: bool, - rustdoc_head: String, - rustdoc_body: String, - rustdoc_body_class: String, - krate: CrateDetails, -} - -impl_webpage! { - RustdocPage = "rustdoc/page.html", -} - /// Serves documentation generated by rustdoc. /// /// This includes all HTML files for an individual crate, as well as the `search-index.js`, which is @@ -233,7 +245,7 @@ pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult { vers, path.join("/") ); - let url = ctry!(req, Url::parse(&redirect_path)); + let url = ctry!(Url::parse(&redirect_path)); Ok(super::redirect(url)) }; @@ -273,11 +285,11 @@ pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult { // Get the crate's details from the database // NOTE: we know this crate must exist because we just checked it above (or else `match_version` is buggy) - let krate = cexpect!(req, CrateDetails::new(&conn, &name, &version)); + let crate_details = cexpect!(CrateDetails::new(&conn, &name, &version)); // if visiting the full path to the default target, remove the target from the path // expects a req_path that looks like `[/:target]/.*` - if req_path.get(0).copied() == Some(&krate.metadata.default_target) { + if req_path.get(0).copied() == Some(&crate_details.metadata.default_target) { return redirect(&name, &version, &req_path[1..]); } @@ -316,22 +328,21 @@ pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult { rendering_time.step("parse html"); - let file_content = ctry!(req, String::from_utf8(file.0.content)); + let file_content = ctry!(String::from_utf8(file.0.content)); // Extract the head and body of the rustdoc file so that we can insert it into our own html - let (rustdoc_head, rustdoc_body, mut rustdoc_body_class) = - ctry!(req, utils::extract_head_and_body(&file_content)); + let (head, body, mut body_class) = ctry!(utils::extract_head_and_body(&file_content)); // Add the `rustdoc` classes to the html body - if rustdoc_body_class.is_empty() { - rustdoc_body_class = "rustdoc container-rustdoc".to_string(); + if body_class.is_empty() { + body_class = "rustdoc container-rustdoc".to_string(); } else { // rustdoc adds its own "rustdoc" class to the body - rustdoc_body_class.push_str(" container-rustdoc"); + body_class.push_str(" container-rustdoc"); } rendering_time.step("find latest path"); - let latest_release = krate.latest_release(); + let latest_release = crate_details.latest_release(); // Get the latest version of the crate let latest_version = latest_release.version.to_owned(); @@ -351,7 +362,7 @@ pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult { "/{}/{}/{}", name, latest_version, - path_for_version(&latest_path, &krate.doc_targets, &conn, &config) + path_for_version(&latest_path, &crate_details.doc_targets, &conn, &config) ) } else { format!("/crate/{}/{}", name, latest_version) @@ -366,7 +377,7 @@ pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult { // Drop the `rustdoc/:crate/:version[/:platform]` prefix inner_path.drain(..3).for_each(drop); - if inner_path.len() > 1 && krate.doc_targets.iter().any(|s| s == inner_path[0]) { + if inner_path.len() > 1 && crate_details.doc_targets.iter().any(|s| s == inner_path[0]) { inner_path.remove(0); } @@ -374,17 +385,27 @@ pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult { }; // Build the page of documentation - RustdocPage { - latest_path, - latest_version, - inner_path, - is_latest_version, - rustdoc_head, - rustdoc_body, - rustdoc_body_class, - krate, - } - .into_response(req) + let content = RustdocPage { + head, + body, + body_class, + name, + full: file_content, + version, + crate_details: Some(crate_details), + ..Default::default() + }; + + // Build the page served to the user while setting options for templating + Page::new(content) + .set_true("show_package_navigation") + .set_true("package_navigation_documentation_tab") + .set_true("package_navigation_show_platforms_tab") + .set_bool("is_latest_version", is_latest_version) + .set("latest_path", &latest_path) + .set("latest_version", &latest_version) + .set("inner_path", &inner_path) + .to_resp("rustdoc") } /// Checks whether the given path exists. @@ -437,8 +458,8 @@ fn path_for_version( pub fn target_redirect_handler(req: &mut Request) -> IronResult { let router = extension!(req, Router); - let name = cexpect!(req, router.find("name")); - let version = cexpect!(req, router.find("version")); + let name = cexpect!(router.find("name")); + let version = cexpect!(router.find("version")); let conn = extension!(req, Pool).get()?; let config = extension!(req, Config); @@ -475,7 +496,7 @@ pub fn target_redirect_handler(req: &mut Request) -> IronResult { path = path ); - let url = ctry!(req, Url::parse(&url)); + let url = ctry!(Url::parse(&url)); let mut resp = Response::with((status::Found, Redirect(url))); resp.headers.set(Expires(HttpDate(time::now()))); @@ -488,27 +509,24 @@ pub fn badge_handler(req: &mut Request) -> IronResult { use params::{Params, Value}; let version = { - let params = ctry!(req, req.get_ref::()); + let params = ctry!(req.get_ref::()); match params.find(&["version"]) { Some(&Value::String(ref version)) => version.clone(), _ => "*".to_owned(), } }; - let name = cexpect!(req, extension!(req, Router).find("crate")); + let name = cexpect!(extension!(req, Router).find("crate")); let conn = extension!(req, Pool).get()?; let options = match match_version(&conn, &name, Some(&version)).and_then(|m| m.assume_exact()) { Some(MatchSemver::Exact((version, id))) => { - let rows = ctry!( - req, - conn.query( - "SELECT rustdoc_status - FROM releases - WHERE releases.id = $1", - &[&id] - ), - ); + let rows = ctry!(conn.query( + "SELECT rustdoc_status + FROM releases + WHERE releases.id = $1", + &[&id] + )); if !rows.is_empty() && rows.get(0).get(0) { BadgeOptions { subject: "docs".to_owned(), @@ -526,11 +544,11 @@ pub fn badge_handler(req: &mut Request) -> IronResult { Some(MatchSemver::Semver((version, _))) => { let base_url = format!("{}/{}/badge.svg", redirect_base(req), name); - let url = ctry!( - req, - iron::url::Url::parse_with_params(&base_url, &[("version", version)]), - ); - let iron_url = ctry!(req, Url::from_generic_url(url)); + let url = ctry!(iron::url::Url::parse_with_params( + &base_url, + &[("version", version)] + )); + let iron_url = ctry!(Url::from_generic_url(url)); return Ok(super::redirect(iron_url)); } @@ -541,7 +559,7 @@ pub fn badge_handler(req: &mut Request) -> IronResult { }, }; - let mut resp = Response::with((status::Ok, ctry!(req, Badge::new(options)).to_svg())); + let mut resp = Response::with((status::Ok, ctry!(Badge::new(options)).to_svg())); resp.headers .set(ContentType("image/svg+xml".parse().unwrap())); resp.headers.set(Expires(HttpDate(time::now()))); @@ -581,9 +599,12 @@ impl Handler for SharedResourceHandler { #[cfg(test)] mod test { + use super::*; use crate::test::*; + use chrono::Utc; use kuchiki::traits::TendrilSink; use reqwest::StatusCode; + use serde_json::json; use std::{collections::BTreeMap, iter::FromIterator}; fn try_latest_version_redirect( @@ -1427,4 +1448,101 @@ mod test { Ok(()) }) } + + #[test] + fn serialize_rustdoc_page() { + let time = Utc::now(); + + let details = json!({ + "name": "rcc", + "version": "100.0.0", + "description": null, + "authors": [], + "owners": [], + "authors_json": null, + "dependencies": null, + "release_time": super::super::duration_to_str(time), + "build_status": true, + "last_successful_build": null, + "rustdoc_status": true, + "repository_url": null, + "homepage_url": null, + "keywords": null, + "have_examples": true, + "target_name": "x86_64-unknown-linux-gnu", + "releases": [], + "github": true, + "yanked": false, + "github_stars": null, + "github_forks": null, + "github_issues": null, + "metadata": { + "name": "serde", + "version": "1.0.0", + "description": "serde does stuff", + "target_name": null, + "rustdoc_status": true, + "default_target": "x86_64-unknown-linux-gnu" + }, + "is_library": true, + "doc_targets": [], + "license": null, + "documentation_url": null + }); + + let mut page = RustdocPage { + head: "Whee".to_string(), + body: "

idk

".to_string(), + body_class: "docsrs-body".to_string(), + name: "rcc".to_string(), + full: "??".to_string(), + version: "100.0.100".to_string(), + description: Some("a Rust compiler in C. Wait, maybe the other way around".to_string()), + crate_details: Some(CrateDetails::default_tester(time)), + }; + + let correct_json = json!({ + "rustdoc_head": "Whee", + "rustdoc_body": "

idk

", + "rustdoc_body_class": "docsrs-body", + "rustdoc_full": "??", + "rustdoc_status": true, + "name": "rcc", + "version": "100.0.100", + "description": "a Rust compiler in C. Wait, maybe the other way around", + "crate_details": details + }); + + assert_eq!(correct_json, serde_json::to_value(&page).unwrap()); + + page.description = None; + let correct_json = json!({ + "rustdoc_head": "Whee", + "rustdoc_body": "

idk

", + "rustdoc_body_class": "docsrs-body", + "rustdoc_full": "??", + "rustdoc_status": true, + "name": "rcc", + "version": "100.0.100", + "description": null, + "crate_details": details + }); + + assert_eq!(correct_json, serde_json::to_value(&page).unwrap()); + + page.crate_details = None; + let correct_json = json!({ + "rustdoc_head": "Whee", + "rustdoc_body": "

idk

", + "rustdoc_body_class": "docsrs-body", + "rustdoc_full": "??", + "rustdoc_status": true, + "name": "rcc", + "version": "100.0.100", + "description": null, + "crate_details": null + }); + + assert_eq!(correct_json, serde_json::to_value(&page).unwrap()); + } } diff --git a/src/web/sitemap.rs b/src/web/sitemap.rs index c612528a6..ad0a6f1f7 100644 --- a/src/web/sitemap.rs +++ b/src/web/sitemap.rs @@ -67,10 +67,7 @@ impl_webpage!(About = "core/about.html"); pub fn about_handler(req: &mut Request) -> IronResult { let conn = extension!(req, Pool).get()?; - let res = ctry!( - req, - conn.query("SELECT value FROM config WHERE name = 'rustc_version'", &[]), - ); + let res = ctry!(conn.query("SELECT value FROM config WHERE name = 'rustc_version'", &[])); let rustc_version = res.iter().next().and_then(|row| { if let Some(Ok(Value::String(version))) = row.get_opt(0) { diff --git a/src/web/source.rs b/src/web/source.rs index 912f78c5f..49bca35e1 100644 --- a/src/web/source.rs +++ b/src/web/source.rs @@ -1,34 +1,69 @@ //! Source code browser -use crate::{ - db::Pool, - impl_webpage, - web::{error::Nope, file::File as DbFile, page::WebPage, MetaData}, - Config, -}; -use iron::{status::Status, IronError, IronResult, Request, Response}; +use super::file::File as DbFile; +use super::page::Page; +use super::MetaData; +use crate::db::Pool; +use crate::Config; +use iron::prelude::*; use postgres::Connection; use router::Router; -use serde::Serialize; +use serde::ser::{Serialize, SerializeStruct, Serializer}; use serde_json::Value; use std::cmp::Ordering; +use std::collections::HashMap; -/// A source file's name and mime type -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Serialize)] +/// A source file's type +#[derive(PartialEq, PartialOrd)] +enum FileType { + Dir, + Text, + Binary, + RustSource, +} + +/// A source file +#[derive(PartialEq, PartialOrd)] struct File { - /// The name of the file name: String, - /// The mime type of the file - mime: String, + file_type: FileType, } /// A list of source files -#[derive(Debug, Clone, PartialEq, Serialize)] struct FileList { metadata: MetaData, files: Vec, } +impl Serialize for FileList { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("FileList", 2)?; + state.serialize_field("metadata", &self.metadata)?; + + let mut files = Vec::with_capacity(self.files.len()); + for file in &self.files { + let mut map = HashMap::with_capacity(2); + map.insert("name", Value::String(file.name.to_owned())); + + let file_type = match file.file_type { + FileType::Dir => "file_type_dir", + FileType::Text => "file_type_text", + FileType::Binary => "file_type_binary", + FileType::RustSource => "file_type_rust_source", + }; + map.insert(file_type, Value::Bool(true)); + + files.push(map); + } + state.serialize_field("files", &files)?; + + state.end() + } +} + impl FileList { /// Gets FileList from a request path /// @@ -91,15 +126,19 @@ impl FileList { let path_splited: Vec<&str> = path.split('/').collect(); // if path have '/' it is a directory - let mime = if path_splited.len() > 1 { - "dir".to_owned() + 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 { - mime.to_owned() + FileType::Binary }; let file = File { name: path_splited[0].to_owned(), - mime, + file_type: ftype, }; // avoid adding duplicates, a directory may occur more than once @@ -116,9 +155,9 @@ impl FileList { file_list.sort_by(|a, b| { // directories must be listed first - if a.mime == "dir" && b.mime != "dir" { + if a.file_type == FileType::Dir && b.file_type != FileType::Dir { Ordering::Less - } else if a.mime != "dir" && b.mime == "dir" { + } else if a.file_type != FileType::Dir && b.file_type == FileType::Dir { Ordering::Greater } else { a.name.to_lowercase().cmp(&b.name.to_lowercase()) @@ -142,22 +181,10 @@ impl FileList { } } -#[derive(Debug, Clone, PartialEq, Serialize)] -struct SourcePage { - file_list: FileList, - show_parent_link: bool, - file_content: Option, - is_rust_source: bool, -} - -impl_webpage! { - SourcePage = "crate/source.html", -} - pub fn source_browser_handler(req: &mut Request) -> IronResult { let router = extension!(req, Router); - let name = cexpect!(req, router.find("name")); - let version = cexpect!(req, router.find("version")); + let name = cexpect!(router.find("name")); + let version = cexpect!(router.find("version")); // get path (req_path) for FileList::from_path and actual path for super::file::File::from_path let (req_path, file_path) = { @@ -195,7 +222,7 @@ pub fn source_browser_handler(req: &mut Request) -> IronResult { None }; - let (file_content, is_rust_source) = if let Some(file) = file { + 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.0.mime.starts_with("text") && !file.is_empty() { return Ok(file.serve()); @@ -211,21 +238,77 @@ pub fn source_browser_handler(req: &mut Request) -> IronResult { (None, false) }; - let file_list = FileList::from_path(&conn, &name, &version, &req_path) - .ok_or_else(|| IronError::new(Nope::NoResults, Status::NotFound))?; + let list = FileList::from_path(&conn, &name, &version, &req_path); + if list.is_none() { + use super::error::Nope; + use iron::status; + return Err(IronError::new(Nope::NoResults, status::NotFound)); + } + + 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"); - SourcePage { - file_list, - show_parent_link: !req_path.is_empty(), - file_content, - is_rust_source, + 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") } - .into_response(req) } #[cfg(test)] mod tests { + use super::*; use crate::test::{assert_success, wrapper}; + use serde_json::json; + + #[test] + fn serialize_file_list() { + let file_list = FileList { + metadata: MetaData { + name: "rcc".to_string(), + version: "0.0.0".to_string(), + description: Some("it compiles an unholy language".to_string()), + target_name: None, + rustdoc_status: true, + default_target: "x86_64-unknown-linux-gnu".to_string(), + }, + files: vec![ + File { + name: "main.rs".to_string(), + file_type: FileType::RustSource, + }, + File { + name: "lib.rs".to_string(), + file_type: FileType::RustSource, + }, + ], + }; + + let correct_json = json!({ + "metadata": { + "name": "rcc", + "version": "0.0.0", + "description": "it compiles an unholy language", + "target_name": null, + "rustdoc_status": true, + "default_target": "x86_64-unknown-linux-gnu" + }, + "files": [{ + "name": "main.rs", + "file_type_rust_source": true + }, { + "name": "lib.rs", + "file_type_rust_source": true + }], + }); + + assert_eq!(correct_json, serde_json::to_value(&file_list).unwrap(),); + } #[test] fn cargo_ok_not_skipped() { diff --git a/templates/builds.hbs b/templates/builds.hbs new file mode 100644 index 000000000..0fefd76d6 --- /dev/null +++ b/templates/builds.hbs @@ -0,0 +1,66 @@ +{{> header}} + +{{#with content}} +
+
+ + {{#if build_details}} +
+ Build #{{build_details.id}} {{build_details.build_time}} +
+
$ rustc --version
+{{build_details.rustc_version}}
+$ cratesfyi --version
+{{build_details.cratesfyi_version}}
+$ cratesfyi ...
+{{build_details.output}}
+ {{/if}} + +
+ Builds +
+ + + +
+

{{metadata.name}}'s sandbox limits

+ +

+ All the builds on docs.rs are executed inside a sandbox with limited + resources. The limits for this crate are the following: +

+ + + + {{#each limits}} + + + + + {{/each}} + +
{{{@key}}}{{this}}
+

+ If a build fails because it hit one of those limits please + open an issue + to get them increased. +

+
+
+
+{{/with}} + +{{> footer}} diff --git a/templates/crate/builds.html b/templates/crate/builds.html deleted file mode 100644 index 2982ec495..000000000 --- a/templates/crate/builds.html +++ /dev/null @@ -1,84 +0,0 @@ -{%- extends "base.html" -%} -{%- import "header/package_navigation.html" as navigation -%} - -{%- block title -%} - {{ macros::doc_title(name=metadata.name, version=metadata.version) }} -{%- endblock title -%} - -{%- block header -%} - {{ navigation::package_navigation(metadata=metadata, active_tab="builds") }} -{%- endblock header -%} - -{%- block body -%} -
-
- {# If there is a build log then show it #} - {# TODO: When viewing a build log, show a back button or a hide button #} - {%- if build_details -%} -
- Build #{{ build_details.id }} {{ build_details.build_time | date(format="%+") }} -
- - {%- filter dedent -%} -
-                        # rustc version
-                        {{ build_details.rustc_version }}
-                        # docs.rs version
-                        {{ build_details.docsrs_version }}
-
-                        # build log
-                        {{ build_details.output }}
-                    
- {%- endfilter -%} - {%- endif -%} - -
- Builds -
- - - -
- {# BuildsPage.metadata is an `Option`, so accessing it can fail #} - {%- if metadata -%} -

{{ metadata.name }}'s sandbox limits

- {%- else -%} -

Sandbox limits

- {%- endif -%} - -

- All the builds on docs.rs are executed inside a sandbox with limited - resources. The limits for this crate are the following: -

- - {{ macros::crate_limits(limits=limits) }} - -

- If a build fails because it hit one of those limits please - open an issue - to get them increased. -

-
-
-
-{%- endblock body -%} diff --git a/templates/crate/details.html b/templates/crate/details.html deleted file mode 100644 index 7db4d6602..000000000 --- a/templates/crate/details.html +++ /dev/null @@ -1,177 +0,0 @@ -{%- extends "base.html" -%} -{%- import "header/package_navigation.html" as navigation -%} - -{%- block title -%} - {{ macros::doc_title(name=details.name, version=details.version) }} -{%- endblock title -%} - -{%- block header -%} - {# Set the active tab to the `crate` tab #} - {{ navigation::package_navigation(metadata=details.metadata, active_tab="crate") }} -{%- endblock header -%} - -{%- block body -%} -
-
-
-
- -
-
- -
- {# If the release is not a library #} - {%- if not details.is_library -%} -
- {{ details.name }}-{{ details.version }} is not a library. -
- - {# If the release has been yanked and is a library #} - {%- elif details.yanked -%} -
- {{ details.name }}-{{ details.version }} has been yanked. -
- - {# If the build succeeded, isn't yanked and is a library #} - {%- elif details.build_status -%} - {# If there are no docs display a warning #} - {%- if not details.rustdoc_status -%} -
{{ details.name }}-{{ details.version }} doesn't have any documentation.
- {%- endif -%} - - {# If there's a readme, display it #} - {%- if details.readme -%} - {{ details.readme | safe }} - - {# If there's not a readme then attempt to display the long description #} - {%- elif details.rustdoc -%} - {{ details.rustdoc }} - {%- endif -%} - - {# If the build failed, the release isn't yanked and the release is a library #} - {%- else -%} - {# Display a warning telling the user we failed to build the docs #} -
- docs.rs failed to build {{ details.name }}-{{ details.version }}
Please check the - build logs and, if you believe this is - docs.rs' fault, open an issue. -
- - {# If there is one, display the most next most recent successful build #} - {%- if details.last_successful_build -%} - - {%- endif -%} - {%- endif -%} -
-
-
-{%- endblock body -%} - -{%- block css -%} - {{ macros::highlight_css() }} -{%- endblock css -%} - -{%- block javascript -%} - {# Enable and load Rust and TOML syntax highlighting #} - {{ macros::highlight_js(languages=["rust", "ini"]) }} -{% endblock javascript -%} diff --git a/templates/crate/source.html b/templates/crate/source.html deleted file mode 100644 index ea87a06d1..000000000 --- a/templates/crate/source.html +++ /dev/null @@ -1,105 +0,0 @@ -{%- extends "base.html" -%} -{%- import "header/package_navigation.html" as navigation -%} - -{%- block title -%} - {{ macros::doc_title(name=file_list.metadata.name, version=file_list.metadata.version) }} -{%- endblock title -%} - -{%- block header -%} - {# Set the active tab to the `source` tab #} - {{ navigation::package_navigation(metadata=file_list.metadata, active_tab="source") }} -{%- endblock header -%} - -{%- block body -%} -
-
-
-
- -
-
- - {# If the file has content, then display it in a codeblock #} - {%- if file_content -%} -
-
{{ file_content }}
-
- {%- endif -%} -
-
-{%- endblock body -%} - -{%- block css -%} - {# Highlight.js CSS #} - {{ macros::highlight_css() }} -{%- endblock css -%} - -{%- block javascript -%} - {# Highlight.js JavaScript #} - {{ macros::highlight_js(languages=["rust", "ini", "markdown"]) }} -{%- endblock javascript -%} diff --git a/templates/crate_details.hbs b/templates/crate_details.hbs new file mode 100644 index 000000000..1a1385708 --- /dev/null +++ b/templates/crate_details.hbs @@ -0,0 +1,105 @@ +{{> header}} + + +{{#with content}} +
+
+
+
+ +
+
+
+ {{#unless is_library}} +
{{name}}-{{version}} is not a library.
+ {{else}} + {{#if yanked}} +
{{name}}-{{version}} has been yanked.
+ {{else}} + {{#unless build_status}} +
docs.rs failed to build {{name}}-{{version}}
Please check the build logs and, if you believe this is docs.rs' fault, open an issue.
+ {{#if last_successful_build}} +
Visit the last successful build: {{name}}-{{last_successful_build}}
+ {{/if}} + {{else}} + {{#unless rustdoc_status}} +
{{name}}-{{version}} doesn't have any documentation.
+ {{/unless}} + {{/unless}} + {{/if}} + {{/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..5a1ca0840 --- /dev/null +++ b/templates/error.hbs @@ -0,0 +1,3 @@ +{{> header}} +{{content}} +{{> footer}} diff --git a/templates/error.html b/templates/error.html deleted file mode 100644 index 05a5c8dfb..000000000 --- a/templates/error.html +++ /dev/null @@ -1,12 +0,0 @@ -{%- extends "base.html" -%} - -{%- block header -%} -
-
-

{{ title }}

-
- {{ message | default(value="") }} -
-
-
-{%- endblock header -%} diff --git a/templates/footer.hbs b/templates/footer.hbs new file mode 100644 index 000000000..17ca3265c --- /dev/null +++ b/templates/footer.hbs @@ -0,0 +1,5 @@ +{{#if varsb.javascript_highlightjs}}{{/if}} + + + + diff --git a/templates/header.hbs b/templates/header.hbs new file mode 100644 index 000000000..d1a001cfd --- /dev/null +++ b/templates/header.hbs @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + {{#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/header/package_navigation.html b/templates/header/package_navigation.html deleted file mode 100644 index d832e660b..000000000 --- a/templates/header/package_navigation.html +++ /dev/null @@ -1,99 +0,0 @@ -{# - The standard package navigation menu - - * `title` A possibly-null string. If it is null, `metadata.name metadata.version` will be used as the title - * `metadata` A non-null instance of the MetaData struct - * `platforms` A possibly-null vector of strings - * `active_tab` A string with one of the following values: - * `crate` - * `source` - * `builds` - - Note: `false` here is acting as a pseudo-null value since you can't directly construct null values - and tera requires all parameters without defaults to be filled -#} -{% macro package_navigation(title=false, metadata, platforms=false, active_tab) %} -
-
- {# Page title #} -

- {%- if title -%} - {{ title }} - {%- else -%} - {{ metadata.name }} {{ metadata.version }} - - {%- endif -%} -

- - {# Page description #} -
- {%- if metadata.description -%} - {{ metadata.description }} - {%- endif -%} -
- -
- {# If there are platforms, show a dropdown with them #} - {%- if platforms -%} - - {%- endif -%} - -
    - {# The partial path of the crate, `:name/:release` #} - {%- set crate_path = metadata.name ~ "/" ~ metadata.version -%} - - {# If docs are built, show a tab for them #} - {%- if metadata.rustdoc_status -%} -
  • - {# The docs tab redirects to the docs, so the tab will never be selected and seen #} - - - Documentation - -
  • - {%- endif -%} - - {# The crate information tab #} -
  • - - Crate - -
  • - - {# The source view tab #} -
  • - - - Source - -
  • - - {# The builds tab #} -
  • - - - Builds - -
  • -
-
-
-
-{% endmacro package_navigation %} diff --git a/templates/macros.html b/templates/macros.html deleted file mode 100644 index bd6964731..000000000 --- a/templates/macros.html +++ /dev/null @@ -1,122 +0,0 @@ -{# - Makes the appropriate JS imports for highlighting - * `languages` An array of strings where each is a valid highlight.js language -#} -{% macro highlight_js(languages) %} - {# Load the highlight script #} - - - {# Load the script for each provided language #} - {%- for language in languages -%} - - {%- endfor -%} - - {# Activate highlighting #} - -{% endmacro highlight_js %} - -{# Makes the appropriate CSS imports for highlighting #} -{% macro highlight_css() %} - {# Load the highlighting theme css #} - -{% endmacro highlight_css %} - -{# - Creates a formatted table showing the resource limits of a crate - * `limits` A non-null `Limits` struct -#} -{% macro crate_limits(limits) %} - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Available RAM{{ limits.memory | filesizeformat }}
Maximum rustdoc execution time{{ limits.timeout.secs | timeformat }}
Maximum size of a build log{{ limits.max_log_size | filesizeformat }}
Network access - {%- if limits.networking -%} - allowed - {%- else -%} - blocked - {%- endif -%} -
Maximum number of build targets{{ limits.targets }}
-{% endmacro crate_limits %} - -{# Constructs a title based on the given crate name and version #} -{% macro doc_title(name, version) %} - {%- if name -%} - {{ name }} {{ version | default(value="") }} - Docs.rs - {%- else -%} - Docs.rs - {%- endif -%} -{% endmacro doc_title %} - -{# - Constructs a list of a crate's releases - * `name` The crate's name as a string - * `releases` A list of crate releases where each release has the following fields: - * `version` A string of the release's version - * `yanked` A boolean of the release's yanked status - * `build_status` A boolean of the crate's build status (true for built, false for failed build) -#} -{% macro releases_list(name, releases) %} - {%- for release in releases -%} - {# The url for the release, `/crate/:name/:version` #} - {%- set release_url = "/crate/" ~ name ~ "/" ~ release.version -%} - {# The release's name and version, `:name-:version` #} - {%- set release_name = name ~ "-" ~ release.version -%} - -
  • - {# If the release has been yanked and failed to build, display a warning #} - {%- if release.yanked and release.build_status -%} - - {{ release.version }} - - - {# If the release is yanked but built, display an warning #} - {%- elif release.yanked and not release.build_status -%} - - {{ release.version }} - - - {# If the release failed to build, display a warning #} - {%- elif release.build_status -%} - - {{ release.version }} - - - {# Otherwise just display the version #} - {%- else -%} - - {{ release.version }} - - {%- endif -%} -
  • - {%- endfor -%} -{% endmacro releases_list %} diff --git a/templates/navigation.hbs b/templates/navigation.hbs new file mode 100644 index 000000000..eefaa13d8 --- /dev/null +++ b/templates/navigation.hbs @@ -0,0 +1,93 @@ + + + {{#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/navigation_global_alert.hbs b/templates/navigation_global_alert.hbs new file mode 100644 index 000000000..428bb02c3 --- /dev/null +++ b/templates/navigation_global_alert.hbs @@ -0,0 +1,9 @@ +{{#if ../has_global_alert}} +
  • + + + {{../global_alert.text}} + +
  • +{{/if}} + diff --git a/templates/navigation_rustdoc.hbs b/templates/navigation_rustdoc.hbs new file mode 100644 index 000000000..f17aedbdc --- /dev/null +++ b/templates/navigation_rustdoc.hbs @@ -0,0 +1,139 @@ + diff --git a/templates/releases/activity.html b/templates/releases/activity.html deleted file mode 100644 index ea11f1e62..000000000 --- a/templates/releases/activity.html +++ /dev/null @@ -1,59 +0,0 @@ -{%- extends "base.html" -%} -{%- import "releases/header.html" as release_macros -%} - -{%- block title -%}Releases - Docs.rs{%- endblock title -%} - -{%- block header -%} - {{ release_macros::header(title="Releases", description=description, tab="activity") }} -{%- endblock header -%} - -{%- block body -%} -
    -
    -
    -{%- endblock body -%} - -{# TODO: Do this with tera alone #} -{%- block javascript -%} - - - -{%- endblock javascript -%} diff --git a/templates/releases/build_queue.html b/templates/releases/build_queue.html deleted file mode 100644 index 37366aea9..000000000 --- a/templates/releases/build_queue.html +++ /dev/null @@ -1,38 +0,0 @@ -{%- extends "base.html" -%} -{%- import "releases/header.html" as release_macros -%} - -{%- block title -%}Build Queue - Docs.rs{%- endblock title -%} - -{%- block header -%} - {{ release_macros::header(title="Build Queue", description=description, tab="queue") }} -{%- endblock header -%} - -{%- block body -%} -
    -
    - -
    - {% set queue_length = queue | length -%} - {%- if queue_length == 0 -%} - There is nothing in the queue - {%- else -%} - Queue - {%- endif %} -
    - -
      - {% for crate in queue -%} -
    1. - - {{ crate.name }} {{ crate.version }} - - - {% if crate.priority != 0 -%} - (priority: {{ crate.priority }}) - {%- endif %} -
    2. - {%- endfor %} -
    -
    -
    -{%- endblock body -%} diff --git a/templates/releases_activity.hbs b/templates/releases_activity.hbs new file mode 100644 index 000000000..c82617b1a --- /dev/null +++ b/templates/releases_activity.hbs @@ -0,0 +1,41 @@ +{{> header}} +{{#with content}} + +
    +
    +
    + + +{{/with}} +{{> footer}} diff --git a/templates/releases_queue.hbs b/templates/releases_queue.hbs new file mode 100644 index 000000000..438f997e3 --- /dev/null +++ b/templates/releases_queue.hbs @@ -0,0 +1,29 @@ +{{> header}} + +
    +
    + +
    + {{#if varsb.queue_empty}} + There is nothing in queue + {{else}} + Queue + {{/if}} +
    + +
      + {{#each content}} +
    1. + + {{this.name}} {{this.version}} + + {{#if this.priority}} + (priority: {{this.priority}}) + {{/if}} +
    2. + {{/each}} +
    +
    +
    + +{{> footer}} diff --git a/templates/rustdoc.hbs b/templates/rustdoc.hbs new file mode 100644 index 000000000..f39289c3d --- /dev/null +++ b/templates/rustdoc.hbs @@ -0,0 +1,34 @@ + + + + {{{content.rustdoc_head}}} + + + + + + + +{{> navigation_rustdoc}} +
    + {{{content.rustdoc_body}}} +
    + + + + diff --git a/templates/rustdoc/navigation.html b/templates/rustdoc/navigation.html deleted file mode 100644 index 94c0efddb..000000000 --- a/templates/rustdoc/navigation.html +++ /dev/null @@ -1,220 +0,0 @@ -{%- import "macros.html" as macros -%} - -{# The url of the current release, `/crate/:name/:version` #} -{%- set crate_url = "/crate/" ~ krate.name ~ "/" ~ krate.version -%} - - diff --git a/templates/rustdoc/page.html b/templates/rustdoc/page.html deleted file mode 100644 index 1cac66fd3..000000000 --- a/templates/rustdoc/page.html +++ /dev/null @@ -1,56 +0,0 @@ -{%- import "macros.html" as macros -%} - - - - - - {# Include any head that rustdoc requires #} - {{ rustdoc_head | safe }} - - - - - - - - {# Highlight.js CSS #} - {{ macros::highlight_css() }} - - {{ macros::doc_title(name=krate.name, version=krate.version) }} - - - - {%- include "rustdoc/navigation.html" -%} - - {# Make the body of the page the documentation generated by rustdoc #} -
    - {{ rustdoc_body | safe }} -
    - - - - - - - {# Highlight.js JavaScript #} - {{ macros::highlight_js(languages=["rust", "ini"]) }} - - 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/base.html b/tera-templates/base.html similarity index 100% rename from templates/base.html rename to tera-templates/base.html diff --git a/templates/core/Cargo.toml.example b/tera-templates/core/Cargo.toml.example similarity index 100% rename from templates/core/Cargo.toml.example rename to tera-templates/core/Cargo.toml.example diff --git a/templates/core/about.html b/tera-templates/core/about.html similarity index 100% rename from templates/core/about.html rename to tera-templates/core/about.html diff --git a/templates/core/home.html b/tera-templates/core/home.html similarity index 99% rename from templates/core/home.html rename to tera-templates/core/home.html index 0ce0164c0..2ef1435ba 100644 --- a/templates/core/home.html +++ b/tera-templates/core/home.html @@ -77,7 +77,6 @@

    Docs.rs

    if ("key" in ev && typeof ev.key != "undefined") { return ev.key; } - return String.fromCharCode(ev.charCode || ev.keyCode); } @@ -95,7 +94,6 @@

    Docs.rs

    if (ev.ctrlKey || ev.altKey || ev.metaKey || document.activeElement.tagName === "INPUT") { return; } - switch (getKey(ev)) { case "s": case "S": diff --git a/templates/core/sitemap.xml b/tera-templates/core/sitemap.xml similarity index 100% rename from templates/core/sitemap.xml rename to tera-templates/core/sitemap.xml diff --git a/templates/header/global_alert.html b/tera-templates/header/global_alert.html similarity index 100% rename from templates/header/global_alert.html rename to tera-templates/header/global_alert.html diff --git a/templates/header/topbar.html b/tera-templates/header/topbar.html similarity index 100% rename from templates/header/topbar.html rename to tera-templates/header/topbar.html diff --git a/tera-templates/macros.html b/tera-templates/macros.html new file mode 100644 index 000000000..7aa586290 --- /dev/null +++ b/tera-templates/macros.html @@ -0,0 +1,68 @@ +{# + Makes the appropriate JS imports for highlighting + * `languages` An array of strings where each is a valid highlight.js language +#} +{% macro highlight_js(languages) %} + {# Load the highlight script #} + + + {# Load the script for each provided language #} + {%- for language in languages -%} + + {%- endfor -%} + + {# Activate highlighting #} + +{% endmacro highlight_js %} + +{# Makes the appropriate CSS imports for highlighting #} +{% macro highlight_css() %} + {# Load the highlighting theme css #} + +{% endmacro highlight_css %} + +{# + Creates a formatted table showing the resource limits of a crate + * `limits` A non-null `Limits` struct +#} +{% macro crate_limits(limits) %} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Available RAM{{ limits.memory | filesizeformat }}
    Maximum rustdoc execution time{{ limits.timeout.secs | timeformat }}
    Maximum size of a build log{{ limits.max_log_size | filesizeformat }}
    Network access + {%- if limits.networking -%} + allowed + {%- else -%} + blocked + {%- endif -%} +
    Maximum number of build targets{{ limits.targets }}
    +{% endmacro crate_limits %} diff --git a/templates/releases/feed.xml b/tera-templates/releases/feed.xml similarity index 100% rename from templates/releases/feed.xml rename to tera-templates/releases/feed.xml diff --git a/templates/releases/header.html b/tera-templates/releases/header.html similarity index 100% rename from templates/releases/header.html rename to tera-templates/releases/header.html diff --git a/templates/releases/releases.html b/tera-templates/releases/releases.html similarity index 64% rename from templates/releases/releases.html rename to tera-templates/releases/releases.html index e4c745f4b..5f6ab863a 100644 --- a/templates/releases/releases.html +++ b/tera-templates/releases/releases.html @@ -87,59 +87,30 @@ return String.fromCharCode(ev.charCode || ev.keyCode); } - var active = null; - function handleKey(ev) { + document.getElementById("i-am-feeling-lucky-button").onclick = function () { + var form = document.getElementsByClassName("landing-search-form"); + var input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'i-am-feeling-lucky'; + input.value = 1; + document.getElementsByClassName("landing-search-form")[0].appendChild(input); + return true; + }; + + function handleShortcut(ev) { if (ev.ctrlKey || ev.altKey || ev.metaKey || document.activeElement.tagName === "INPUT") { return; } - - if (ev.which === 40) { // Down arrow - ev.preventDefault(); - if (active === null) { - active = document.getElementsByClassName("recent-releases-container")[0].getElementsByTagName("li")[0]; - } else if (active.nextElementSibling) { - active.classList.remove("selected"); - active = active.nextElementSibling; - } - active.classList.add("selected"); - } else if (ev.which === 38) { // Up arrow - ev.preventDefault(); - if (active === null) { - active = document.getElementsByClassName("recent-releases-container")[0].getElementsByTagName("li")[0]; - } else if (active.previousElementSibling) { - active.classList.remove("selected"); - active = active.previousElementSibling; - } - active.classList.add("selected"); - active.focus(); - } else if (ev.which === 13) { // Return - if (active !== null) { - document.location.href = active.getElementsByTagName("a")[0].href; - } - } else { - switch (getKey(ev)) { - case "s": - case "S": - ev.preventDefault(); - document.getElementsByClassName("search-input-nav")[0].focus(); - break; - } + switch (getKey(ev)) { + case "s": + case "S": + ev.preventDefault(); + document.getElementById("search").focus(); + break; } } - document.onkeypress = handleKey; - document.onkeydown = handleKey; - - var crates = Array.prototype.slice.call(document.getElementsByClassName("recent-releases-container")[0].getElementsByTagName("li")); - for (var i = 0; i < crates.length; ++i) { - crates[i].addEventListener("mouseover", function (event) { - this.classList.remove("selected"); - active = null; - }); - crates[i].addEventListener("mouseout", function (event) { - this.classList.remove("selected"); - active = null; - }); - } + document.onkeypress = handleShortcut; + document.onkeydown = handleShortcut; {%- endblock javascript -%}