diff --git a/.gitignore b/.gitignore index d7823cbaf..f610a3252 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ target *.css *.css.map +!vendor/**/*.css .sass-cache .vagrant .rustwide diff --git a/README.md b/README.md index 9f257d5f1..3f399161b 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,21 @@ cargo run -- daemon --registry-watcher=disabled cargo run -- queue add ``` +### Updating vendored sources + +The instructions & links for updating Font Awesome can be found [on their website](https://fontawesome.com/how-to-use/on-the-web/using-with/sass). Similarly, Pure-CSS also [explains on theirs](https://purecss.io/start/). + +When updating Font Awesome, make sure to change `$fa-font-path` in `scss/_variables.scss` (it should be at the top of the file) to `../-/static`. This will point font awesome at the correct path from which to request font and icon resources. + + ### Contact Docs.rs is run and maintained by the [docs.rs team](https://www.rust-lang.org/governance/teams/dev-tools#docs-rs). diff --git a/build.rs b/build.rs index 9b68773a9..0a9847a24 100644 --- a/build.rs +++ b/build.rs @@ -22,6 +22,7 @@ fn main() { println!("cargo:rerun-if-changed=templates/style/_navbar.scss"); println!("cargo:rerun-if-changed=templates/menu.js"); println!("cargo:rerun-if-changed=templates/index.js"); + println!("cargo:rerun-if-changed=vendor/"); // TODO: are these right? println!("cargo:rerun-if-changed=.git/HEAD"); println!("cargo:rerun-if-changed=.git/index"); @@ -63,7 +64,11 @@ fn compile_sass() -> Result<(), Box> { let mut context = Context::new_file(format!("{}/base.scss", STYLE_DIR))?; context.set_options(Options { output_style: OutputStyle::Compressed, - include_paths: vec![STYLE_DIR.to_owned()], + include_paths: vec![ + STYLE_DIR.to_owned(), + concat!(env!("CARGO_MANIFEST_DIR"), "/vendor/fontawesome/scss").to_owned(), + concat!(env!("CARGO_MANIFEST_DIR"), "/vendor/pure-css/css").to_owned(), + ], ..Default::default() }); diff --git a/dockerfiles/Dockerfile b/dockerfiles/Dockerfile index f72e59d1c..d6dd4dc22 100644 --- a/dockerfiles/Dockerfile +++ b/dockerfiles/Dockerfile @@ -50,6 +50,7 @@ RUN find src -name "*.rs" -exec touch {} \; COPY templates/style templates/style COPY templates/index.js templates/ COPY templates/menu.js templates/ +COPY vendor vendor/ RUN cargo build --release @@ -75,6 +76,7 @@ COPY --from=build /build/target/release/cratesfyi /usr/local/bin COPY static /opt/docsrs/prefix/public_html COPY templates /opt/docsrs/templates COPY dockerfiles/entrypoint.sh /opt/docsrs/ +COPY vendor vendor/ WORKDIR /opt/docsrs ENTRYPOINT ["/opt/docsrs/entrypoint.sh"] diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 8e9939ec6..61a4a2e3f 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -190,7 +190,7 @@ impl Storage { #[cfg(test)] pub(crate) fn store_blobs(&self, blobs: Vec) -> Result<(), Error> { - self.store_inner(blobs.into_iter().map(|blob| Ok(blob))) + self.store_inner(blobs.into_iter().map(Ok)) } fn store_inner( @@ -276,7 +276,7 @@ mod test { crate::test::init_logger(); let files = get_file_list(env::current_dir().unwrap()); assert!(files.is_ok()); - assert!(files.unwrap().len() > 0); + assert!(!files.unwrap().is_empty()); let files = get_file_list(env::current_dir().unwrap().join("Cargo.toml")).unwrap(); assert_eq!(files[0], std::path::Path::new("Cargo.toml")); @@ -375,7 +375,7 @@ mod backend_tests { compression: None, }; - storage.store_blobs(vec![small_blob.clone(), big_blob.clone()])?; + storage.store_blobs(vec![small_blob.clone(), big_blob])?; let blob = storage.get("small-blob.bin", MAX_SIZE)?; assert_eq!(blob.content.len(), small_blob.content.len()); @@ -480,7 +480,7 @@ mod backend_tests { mime: "text/rust".into(), content, path: format!("{}.rs", i), - date_updated: now.clone(), + date_updated: now, compression: None, } }) diff --git a/src/web/crate_details.rs b/src/web/crate_details.rs index fb02b173a..15178bee0 100644 --- a/src/web/crate_details.rs +++ b/src/web/crate_details.rs @@ -663,7 +663,7 @@ mod tests { .create()?; let details = CrateDetails::new(&mut db.conn(), "foo", "0.0.1").unwrap(); - let mut owners = details.owners.clone(); + let mut owners = details.owners; owners.sort(); assert_eq!( owners, diff --git a/src/web/metrics.rs b/src/web/metrics.rs index 44c26358d..969d854e8 100644 --- a/src/web/metrics.rs +++ b/src/web/metrics.rs @@ -117,8 +117,8 @@ mod tests { ("/", "/"), ("/crate/hexponent/0.2.0", "/crate/:name/:version"), ("/crate/rcc/0.0.0", "/crate/:name/:version"), - ("/index.js", "static resource"), - ("/menu.js", "static resource"), + ("/-/static/index.js", "static resource"), + ("/-/static/menu.js", "static resource"), ("/opensearch.xml", "static resource"), ("/releases", "/releases"), ("/releases/feed", "static resource"), @@ -131,7 +131,7 @@ mod tests { ("/releases/recent/1", "/releases/recent/:page"), ("/robots.txt", "static resource"), ("/sitemap.xml", "static resource"), - ("/style.css", "static resource"), + ("/-/static/style.css", "static resource"), ]; wrapper(|env| { diff --git a/src/web/mod.rs b/src/web/mod.rs index 032df84be..eeaa8edb0 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -87,6 +87,7 @@ mod routes; mod rustdoc; mod sitemap; mod source; +mod statics; use crate::{impl_webpage, Context}; use chrono::{DateTime, Utc}; @@ -110,9 +111,6 @@ use std::{borrow::Cow, env, fmt, net::SocketAddr, path::PathBuf, sync::Arc, time /// Duration of static files for staticfile and DatabaseFileHandler (in seconds) const STATIC_FILE_CACHE_DURATION: u64 = 60 * 60 * 24 * 30 * 12; // 12 months -const STYLE_CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/style.css")); -const MENU_JS: &str = include_str!(concat!(env!("OUT_DIR"), "/menu.js")); -const INDEX_JS: &str = include_str!(concat!(env!("OUT_DIR"), "/index.js")); const OPENSEARCH_XML: &[u8] = include_bytes!("opensearch.xml"); const DEFAULT_BIND: &str = "0.0.0.0:3000"; @@ -495,35 +493,6 @@ fn redirect_base(req: &Request) -> String { } } -fn style_css_handler(_: &mut Request) -> IronResult { - let mut response = Response::with((status::Ok, STYLE_CSS)); - let cache = vec![ - CacheDirective::Public, - CacheDirective::MaxAge(STATIC_FILE_CACHE_DURATION as u32), - ]; - - response - .headers - .set(ContentType("text/css".parse().unwrap())); - response.headers.set(CacheControl(cache)); - - Ok(response) -} - -fn load_js(file_path_str: &'static str) -> IronResult { - let mut response = Response::with((status::Ok, file_path_str)); - let cache = vec![ - CacheDirective::Public, - CacheDirective::MaxAge(STATIC_FILE_CACHE_DURATION as u32), - ]; - response - .headers - .set(ContentType("application/javascript".parse().unwrap())); - response.headers.set(CacheControl(cache)); - - Ok(response) -} - fn opensearch_xml_handler(_: &mut Request) -> IronResult { let mut response = Response::with((status::Ok, OPENSEARCH_XML)); let cache = vec![ @@ -539,26 +508,6 @@ fn opensearch_xml_handler(_: &mut Request) -> IronResult { Ok(response) } -fn ico_handler(req: &mut Request) -> IronResult { - if let Some(&"favicon.ico") = req.url.path().last() { - // if we're looking for exactly "favicon.ico", we need to defer to the handler that loads - // from `public_html`, so return a 404 here to make the main handler carry on - Err(IronError::new( - error::Nope::ResourceNotFound, - status::NotFound, - )) - } 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))), - ); - - Ok(redirect(url)) - } -} - /// MetaData used in header #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub(crate) struct MetaData { diff --git a/src/web/routes.rs b/src/web/routes.rs index a7a8d5d21..bd874abb9 100644 --- a/src/web/routes.rs +++ b/src/web/routes.rs @@ -1,7 +1,5 @@ use super::metrics::RequestRecorder; -use crate::web::{INDEX_JS, MENU_JS}; use iron::middleware::Handler; -use iron::Request; use router::Router; use std::collections::HashSet; @@ -12,12 +10,10 @@ pub(super) const DOC_RUST_LANG_ORG_REDIRECTS: &[&str] = pub(super) fn build_routes() -> Routes { let mut routes = Routes::new(); - routes.static_resource("/style.css", super::style_css_handler); - routes.static_resource("/index.js", |_: &mut Request| super::load_js(INDEX_JS)); - routes.static_resource("/menu.js", |_: &mut Request| super::load_js(MENU_JS)); routes.static_resource("/robots.txt", super::sitemap::robots_txt_handler); routes.static_resource("/sitemap.xml", super::sitemap::sitemap_handler); routes.static_resource("/opensearch.xml", super::opensearch_xml_handler); + routes.static_resource("/-/static/:file", super::statics::static_handler); routes.internal_page("/", super::releases::home_page); diff --git a/src/web/rustdoc.rs b/src/web/rustdoc.rs index d5cb0de22..36eaf0776 100644 --- a/src/web/rustdoc.rs +++ b/src/web/rustdoc.rs @@ -125,7 +125,7 @@ pub fn rustdoc_redirector_handler(req: &mut Request) -> IronResult { { // route .ico files into their dedicated handler so that docs.rs's favicon is always // displayed - return super::ico_handler(req); + return super::statics::ico_handler(req); } let router = extension!(req, Router); diff --git a/src/web/statics.rs b/src/web/statics.rs new file mode 100644 index 000000000..022a99a79 --- /dev/null +++ b/src/web/statics.rs @@ -0,0 +1,252 @@ +use super::{error::Nope, redirect, redirect_base, STATIC_FILE_CACHE_DURATION}; +use chrono::Utc; +use iron::{ + headers::CacheDirective, + headers::{CacheControl, ContentLength, ContentType, LastModified}, + status::Status, + IronError, IronResult, Request, Response, Url, +}; +use mime_guess::MimeGuess; +use router::Router; +use std::{ffi::OsStr, fs, path::Path}; + +const STYLE_CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/style.css")); +const MENU_JS: &str = include_str!(concat!(env!("OUT_DIR"), "/menu.js")); +const INDEX_JS: &str = include_str!(concat!(env!("OUT_DIR"), "/index.js")); +const STATIC_SEARCH_PATHS: &[&str] = &["vendor/fontawesome/webfonts", "vendor/pure-css/css"]; + +pub(crate) fn static_handler(req: &mut Request) -> IronResult { + let router = extension!(req, Router); + let file = cexpect!(req, router.find("file")); + + match file { + "style.css" => serve_resource(STYLE_CSS, ContentType("text/css".parse().unwrap())), + "index.js" => serve_resource( + INDEX_JS, + ContentType("application/javascript".parse().unwrap()), + ), + "menu.js" => serve_resource( + MENU_JS, + ContentType("application/javascript".parse().unwrap()), + ), + + file => serve_file(req, file), + } +} + +fn serve_file(req: &Request, file: &str) -> IronResult { + // Filter out files that attempt to traverse directories + if file.contains("..") || file.contains('/') || file.contains('\\') { + return Err(IronError::new(Nope::ResourceNotFound, Status::NotFound)); + } + + // Find the first path that actually exists + let path = STATIC_SEARCH_PATHS + .iter() + .map(|p| Path::new(p).join(file)) + .find(|p| p.exists()) + .ok_or_else(|| IronError::new(Nope::ResourceNotFound, Status::NotFound))?; + let contents = ctry!(req, fs::read(&path)); + + // If we can detect the file's mime type, set it + // MimeGuess misses a lot of the file types we need, so there's a small wrapper + // around it + let content_type = path + .extension() + .and_then(OsStr::to_str) + .and_then(|ext| match ext { + "eot" => Some(ContentType( + "application/vnd.ms-fontobject".parse().unwrap(), + )), + "woff2" => Some(ContentType("application/font-woff2".parse().unwrap())), + "ttf" => Some(ContentType("application/x-font-ttf".parse().unwrap())), + + _ => MimeGuess::from_path(&path) + .first() + .map(|mime| ContentType(mime.as_ref().parse().unwrap())), + }); + + serve_resource(contents, content_type) +} + +fn serve_resource(resource: R, content_type: C) -> IronResult +where + R: AsRef<[u8]>, + C: Into>, +{ + let mut response = Response::with((Status::Ok, resource.as_ref())); + + let cache = vec![ + CacheDirective::Public, + CacheDirective::MaxAge(STATIC_FILE_CACHE_DURATION as u32), + ]; + response.headers.set(CacheControl(cache)); + + response + .headers + .set(ContentLength(resource.as_ref().len() as u64)); + response.headers.set(LastModified( + Utc::now() + .format("%a, %d %b %Y %T %Z") + .to_string() + .parse() + .unwrap(), + )); + + if let Some(content_type) = content_type.into() { + response.headers.set(content_type); + } + + Ok(response) +} + +pub(super) fn ico_handler(req: &mut Request) -> IronResult { + if let Some(&"favicon.ico") = req.url.path().last() { + // if we're looking for exactly "favicon.ico", we need to defer to the handler that loads + // from `public_html`, so return a 404 here to make the main handler carry on + Err(IronError::new(Nope::ResourceNotFound, Status::NotFound)) + } 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))), + ); + + Ok(redirect(url)) + } +} + +#[cfg(test)] +mod tests { + use super::{INDEX_JS, MENU_JS, STATIC_SEARCH_PATHS, STYLE_CSS}; + use crate::test::wrapper; + use std::fs; + + #[test] + fn style_css() { + wrapper(|env| { + let web = env.frontend(); + + let resp = web.get("/-/static/style.css").send()?; + assert!(resp.status().is_success()); + assert_eq!( + resp.headers().get("Content-Type"), + Some(&"text/css".parse().unwrap()), + ); + assert_eq!(resp.content_length().unwrap(), STYLE_CSS.len() as u64); + assert_eq!(resp.text()?, STYLE_CSS); + + Ok(()) + }); + } + + #[test] + fn index_js() { + wrapper(|env| { + let web = env.frontend(); + + let resp = web.get("/-/static/index.js").send()?; + assert!(resp.status().is_success()); + assert_eq!( + resp.headers().get("Content-Type"), + Some(&"application/javascript".parse().unwrap()), + ); + assert_eq!(resp.content_length().unwrap(), INDEX_JS.len() as u64); + assert_eq!(resp.text()?, INDEX_JS); + + Ok(()) + }); + } + + #[test] + fn menu_js() { + wrapper(|env| { + let web = env.frontend(); + + let resp = web.get("/-/static/menu.js").send()?; + assert!(resp.status().is_success()); + assert_eq!( + resp.headers().get("Content-Type"), + Some(&"application/javascript".parse().unwrap()), + ); + assert_eq!(resp.content_length().unwrap(), MENU_JS.len() as u64); + assert_eq!(resp.text()?, MENU_JS); + + Ok(()) + }); + } + + #[test] + fn static_files() { + wrapper(|env| { + let web = env.frontend(); + + for path in STATIC_SEARCH_PATHS { + for (file, path) in fs::read_dir(path)? + .map(|e| e.unwrap()) + .map(|e| (e.file_name(), e.path())) + { + let url = format!("/-/static/{}", file.to_str().unwrap()); + let resp = web.get(&url).send()?; + + assert!(resp.status().is_success(), "failed to fetch {:?}", url); + assert_eq!( + resp.bytes()?, + fs::read(&path).unwrap(), + "failed to fetch {:?}", + url, + ); + } + } + + Ok(()) + }); + } + + #[test] + fn static_file_that_doesnt_exist() { + wrapper(|env| { + let web = env.frontend(); + assert_eq!( + web.get("/-/static/whoop-de-do.png") + .send()? + .status() + .as_u16(), + 404, + ); + + Ok(()) + }); + } + + #[test] + fn static_mime_types() { + wrapper(|env| { + let web = env.frontend(); + + let files = &[ + ("pure-min.css", "text/css"), + ("fa-brands-400.eot", "application/vnd.ms-fontobject"), + ("fa-brands-400.svg", "image/svg+xml"), + ("fa-brands-400.ttf", "application/x-font-ttf"), + ("fa-brands-400.woff", "application/font-woff"), + ("fa-brands-400.woff2", "application/font-woff2"), + ]; + + for (file, mime) in files { + let url = format!("/-/static/{}", file); + let resp = web.get(&url).send()?; + + assert_eq!( + resp.headers().get("Content-Type"), + Some(&mime.parse().unwrap()), + "{:?} has an incorrect content type", + url, + ); + } + + Ok(()) + }); + } +} diff --git a/templates/base.html b/templates/base.html index 1bf090f9d..e313b3a4e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -9,18 +9,11 @@ {%- block meta -%}{%- endblock meta -%} - {# External styles #} - - - - {# Docs.rs styles #} - + {%- block css -%}{%- endblock css -%} @@ -37,8 +30,8 @@ {%- block body -%}{%- endblock body -%} - - + + {%- block javascript -%}{%- endblock javascript -%} diff --git a/templates/rustdoc/body.html b/templates/rustdoc/body.html index f9a2beb8f..4cfd2c4f4 100644 --- a/templates/rustdoc/body.html +++ b/templates/rustdoc/body.html @@ -1,5 +1,5 @@ - - + +