diff --git a/contrib/lib/Cargo.toml b/contrib/lib/Cargo.toml index 5660ddaf..a792f9ae 100644 --- a/contrib/lib/Cargo.toml +++ b/contrib/lib/Cargo.toml @@ -20,10 +20,9 @@ databases = [ ] # User-facing features. -default = ["serve"] +default = [] tera_templates = ["tera", "templates"] handlebars_templates = ["handlebars", "templates"] -serve = [] compression = ["brotli_compression", "gzip_compression"] brotli_compression = ["brotli"] gzip_compression = ["flate2"] diff --git a/contrib/lib/src/lib.rs b/contrib/lib/src/lib.rs index 944120a3..c02ebf4f 100644 --- a/contrib/lib/src/lib.rs +++ b/contrib/lib/src/lib.rs @@ -16,7 +16,6 @@ //! common modules exposed by default. The present feature list is below, with //! an asterisk next to the features that are enabled by default: //! -//! * [serve*](serve) - Static File Serving //! * [handlebars_templates](templates) - Handlebars Templating //! * [tera_templates](templates) - Tera Templating //! * [${database}_pool](databases) - Database Configuration and Pooling @@ -39,7 +38,6 @@ #[allow(unused_imports)] #[macro_use] extern crate rocket; -#[cfg(feature="serve")] pub mod serve; #[cfg(feature="templates")] pub mod templates; #[cfg(feature="databases")] pub mod databases; // TODO.async: Migrate compression, reenable this, tests, and add to docs. diff --git a/contrib/lib/tests/static_files.rs b/contrib/lib/tests/static_files.rs deleted file mode 100644 index 381d3fbe..00000000 --- a/contrib/lib/tests/static_files.rs +++ /dev/null @@ -1,192 +0,0 @@ -#[cfg(feature = "serve")] -mod static_tests { - use std::{io::Read, fs::File}; - use std::path::Path; - - use rocket::{Rocket, Route, Build}; - use rocket::http::Status; - use rocket::local::blocking::Client; - - use rocket_contrib::serve::{StaticFiles, Options, crate_relative}; - - fn static_root() -> &'static Path { - Path::new(crate_relative!("/tests/static")) - } - - fn rocket() -> Rocket { - let root = static_root(); - rocket::build() - .mount("/default", StaticFiles::from(&root)) - .mount("/no_index", StaticFiles::new(&root, Options::None)) - .mount("/dots", StaticFiles::new(&root, Options::DotFiles)) - .mount("/index", StaticFiles::new(&root, Options::Index)) - .mount("/both", StaticFiles::new(&root, Options::DotFiles | Options::Index)) - .mount("/redir", StaticFiles::new(&root, Options::NormalizeDirs)) - .mount("/redir_index", StaticFiles::new(&root, Options::NormalizeDirs | Options::Index)) - } - - static REGULAR_FILES: &[&str] = &[ - "index.html", - "inner/goodbye", - "inner/index.html", - "other/hello.txt", - ]; - - static HIDDEN_FILES: &[&str] = &[ - ".hidden", - "inner/.hideme", - ]; - - static INDEXED_DIRECTORIES: &[&str] = &[ - "", - "inner/", - ]; - - fn assert_file(client: &Client, prefix: &str, path: &str, exists: bool) { - let full_path = format!("/{}/{}", prefix, path); - let response = client.get(full_path).dispatch(); - if exists { - assert_eq!(response.status(), Status::Ok); - - let mut path = static_root().join(path); - if path.is_dir() { - path = path.join("index.html"); - } - - let mut file = File::open(path).expect("open file"); - let mut expected_contents = String::new(); - file.read_to_string(&mut expected_contents).expect("read file"); - assert_eq!(response.into_string(), Some(expected_contents)); - } else { - assert_eq!(response.status(), Status::NotFound); - } - } - - fn assert_all(client: &Client, prefix: &str, paths: &[&str], exist: bool) { - for path in paths.iter() { - assert_file(client, prefix, path, exist); - } - } - - #[test] - fn test_static_no_index() { - let client = Client::debug(rocket()).expect("valid rocket"); - assert_all(&client, "no_index", REGULAR_FILES, true); - assert_all(&client, "no_index", HIDDEN_FILES, false); - assert_all(&client, "no_index", INDEXED_DIRECTORIES, false); - } - - #[test] - fn test_static_hidden() { - let client = Client::debug(rocket()).expect("valid rocket"); - assert_all(&client, "dots", REGULAR_FILES, true); - assert_all(&client, "dots", HIDDEN_FILES, true); - assert_all(&client, "dots", INDEXED_DIRECTORIES, false); - } - - #[test] - fn test_static_index() { - let client = Client::debug(rocket()).expect("valid rocket"); - assert_all(&client, "index", REGULAR_FILES, true); - assert_all(&client, "index", HIDDEN_FILES, false); - assert_all(&client, "index", INDEXED_DIRECTORIES, true); - - assert_all(&client, "default", REGULAR_FILES, true); - assert_all(&client, "default", HIDDEN_FILES, false); - assert_all(&client, "default", INDEXED_DIRECTORIES, true); - } - - #[test] - fn test_static_all() { - let client = Client::debug(rocket()).expect("valid rocket"); - assert_all(&client, "both", REGULAR_FILES, true); - assert_all(&client, "both", HIDDEN_FILES, true); - assert_all(&client, "both", INDEXED_DIRECTORIES, true); - } - - #[test] - fn test_ranking() { - let root = static_root(); - for rank in -128..128 { - let a = StaticFiles::new(&root, Options::None).rank(rank); - let b = StaticFiles::from(&root).rank(rank); - - for handler in vec![a, b] { - let routes: Vec = handler.into(); - assert!(routes.iter().all(|route| route.rank == rank), "{}", rank); - } - } - } - - #[test] - fn test_forwarding() { - use rocket::{get, routes}; - - #[get("/", rank = 20)] - fn catch_one(value: String) -> String { value } - - #[get("//", rank = 20)] - fn catch_two(a: &str, b: &str) -> String { format!("{}/{}", a, b) } - - let rocket = rocket().mount("/default", routes![catch_one, catch_two]); - let client = Client::debug(rocket).expect("valid rocket"); - - let response = client.get("/default/ireallydontexist").dispatch(); - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.into_string().unwrap(), "ireallydontexist"); - - let response = client.get("/default/idont/exist").dispatch(); - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.into_string().unwrap(), "idont/exist"); - - assert_all(&client, "both", REGULAR_FILES, true); - assert_all(&client, "both", HIDDEN_FILES, true); - assert_all(&client, "both", INDEXED_DIRECTORIES, true); - } - - #[test] - fn test_redirection() { - let client = Client::debug(rocket()).expect("valid rocket"); - - // Redirection only happens if enabled, and doesn't affect index behaviour. - let response = client.get("/no_index/inner").dispatch(); - assert_eq!(response.status(), Status::NotFound); - - let response = client.get("/index/inner").dispatch(); - assert_eq!(response.status(), Status::Ok); - - let response = client.get("/redir/inner").dispatch(); - assert_eq!(response.status(), Status::PermanentRedirect); - assert_eq!(response.headers().get("Location").next(), Some("/redir/inner/")); - - let response = client.get("/redir/inner?foo=bar").dispatch(); - assert_eq!(response.status(), Status::PermanentRedirect); - assert_eq!(response.headers().get("Location").next(), Some("/redir/inner/?foo=bar")); - - let response = client.get("/redir_index/inner").dispatch(); - assert_eq!(response.status(), Status::PermanentRedirect); - assert_eq!(response.headers().get("Location").next(), Some("/redir_index/inner/")); - - // Paths with trailing slash are unaffected. - let response = client.get("/redir/inner/").dispatch(); - assert_eq!(response.status(), Status::NotFound); - - let response = client.get("/redir_index/inner/").dispatch(); - assert_eq!(response.status(), Status::Ok); - - // Root of route is also redirected. - let response = client.get("/no_index").dispatch(); - assert_eq!(response.status(), Status::NotFound); - - let response = client.get("/index").dispatch(); - assert_eq!(response.status(), Status::Ok); - - let response = client.get("/redir").dispatch(); - assert_eq!(response.status(), Status::PermanentRedirect); - assert_eq!(response.headers().get("Location").next(), Some("/redir/")); - - let response = client.get("/redir_index").dispatch(); - assert_eq!(response.status(), Status::PermanentRedirect); - assert_eq!(response.headers().get("Location").next(), Some("/redir_index/")); - } -} diff --git a/core/lib/src/fs/mod.rs b/core/lib/src/fs/mod.rs new file mode 100644 index 00000000..b61e50a0 --- /dev/null +++ b/core/lib/src/fs/mod.rs @@ -0,0 +1,6 @@ +//! File serving, file accepting, and file system types. + +mod server; + +pub use server::*; +pub use server::relative; diff --git a/contrib/lib/src/serve.rs b/core/lib/src/fs/server.rs similarity index 65% rename from contrib/lib/src/serve.rs rename to core/lib/src/fs/server.rs index f57912c4..33058810 100644 --- a/contrib/lib/src/serve.rs +++ b/core/lib/src/fs/server.rs @@ -1,64 +1,220 @@ -//! Custom handler and options for static file serving. -//! -//! See the [`StaticFiles`](crate::serve::StaticFiles) type for further details. -//! -//! # Enabling -//! -//! This module is only available when the `serve` feature is enabled. Enable it -//! in `Cargo.toml` as follows: -//! -//! ```toml -//! [dependencies.rocket_contrib] -//! version = "0.5.0-dev" -//! default-features = false -//! features = ["serve"] -//! ``` - use std::path::{PathBuf, Path}; -use rocket::{Request, Data}; -use rocket::http::{Method, uri::Segments, ext::IntoOwned}; -use rocket::response::{NamedFile, Redirect}; -use rocket::route::{Route, Handler, Outcome}; +use crate::{Request, Data}; +use crate::http::{Method, uri::Segments, ext::IntoOwned}; +use crate::response::{NamedFile, Redirect}; +use crate::route::{Route, Handler, Outcome}; -/// Generates a crate-relative version of `$path`. +/// Custom handler for serving static files. /// -/// This macro is primarily intended for use with [`StaticFiles`] to serve files -/// from a path relative to the crate root. The macro accepts one parameter, -/// `$path`, an absolute or, preferably, relative path. It returns a path (an -/// `&'static str`) prefixed with the path to the crate root. Use `Path::new()` -/// to retrieve an `&'static Path`. +/// This handler makes it simple to serve static files from a directory on the +/// local file system. To use it, construct a `FileServer` using either +/// [`FileServer::from()`] or [`FileServer::new()`] then simply `mount` the +/// handler at a desired path. When mounted, the handler will generate route(s) +/// that serve the desired static files. If a requested file is not found, the +/// routes _forward_ the incoming request. The default rank of the generated +/// routes is `10`. To customize route ranking, use the [`FileServer::rank()`] +/// method. /// -/// See the [relative paths `StaticFiles` -/// documentation](`StaticFiles`#relative-paths) for an example. +/// # Options +/// +/// The handler's functionality can be customized by passing an [`Options`] to +/// [`FileServer::new()`]. /// /// # Example /// -/// ```rust -/// use std::path::Path; -/// use rocket_contrib::serve::crate_relative; +/// To serve files from the `/static` directory on the local file system at the +/// `/public` path, allowing `index.html` files to be used to respond to +/// requests for a directory (the default), you might write the following: /// -/// let manual = Path::new(env!("CARGO_MANIFEST_DIR")).join("static"); -/// let automatic_1 = Path::new(crate_relative!("static")); -/// let automatic_2 = Path::new(crate_relative!("/static")); -/// assert_eq!(manual, automatic_1); -/// assert_eq!(automatic_1, automatic_2); +/// ```rust,no_run +/// # #[macro_use] extern crate rocket; +/// use rocket::fs::FileServer; +/// +/// #[launch] +/// fn rocket() -> _ { +/// rocket::build().mount("/public", FileServer::from("/static")) +/// } /// ``` -#[macro_export] -macro_rules! crate_relative { - ($path:expr) => { - if cfg!(windows) { - concat!(env!("CARGO_MANIFEST_DIR"), "\\", $path) - } else { - concat!(env!("CARGO_MANIFEST_DIR"), "/", $path) - } - }; +/// +/// With this, requests for files at `/public/` will be handled by +/// returning the contents of `/static/`. Requests for _directories_ at +/// `/public/` will be handled by returning the contents of +/// `/static//index.html`. +/// +/// ## Relative Paths +/// +/// In the example above, `/static` is an absolute path. If your static files +/// are stored relative to your crate and your project is managed by Rocket, use +/// the [`relative!`] macro to obtain a path that is relative to your +/// crate's root. For example, to serve files in the `static` subdirectory of +/// your crate at `/`, you might write: +/// +/// ```rust,no_run +/// # #[macro_use] extern crate rocket; +/// use rocket::fs::{FileServer, relative}; +/// +/// #[launch] +/// fn rocket() -> _ { +/// rocket::build().mount("/", FileServer::from(relative!("static"))) +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct FileServer { + root: PathBuf, + options: Options, + rank: isize, } -#[doc(inline)] -pub use crate_relative; +impl FileServer { + /// The default rank use by `FileServer` routes. + const DEFAULT_RANK: isize = 10; -/// A bitset representing configurable options for the [`StaticFiles`] handler. + /// Constructs a new `FileServer` that serves files from the file system + /// `path`. By default, [`Options::Index`] is set, and the generated routes + /// have a rank of `10`. To serve static files with other options, use + /// [`FileServer::new()`]. To choose a different rank for generated routes, + /// use [`FileServer::rank()`]. + /// + /// # Panics + /// + /// Panics if `path` does not exist or is not a directory. + /// + /// # Example + /// + /// Serve the static files in the `/www/public` local directory on path + /// `/static`. + /// + /// ```rust,no_run + /// # #[macro_use] extern crate rocket; + /// use rocket::fs::FileServer; + /// + /// #[launch] + /// fn rocket() -> _ { + /// rocket::build().mount("/static", FileServer::from("/www/public")) + /// } + /// ``` + /// + /// Exactly as before, but set the rank for generated routes to `30`. + /// + /// ```rust,no_run + /// # #[macro_use] extern crate rocket; + /// use rocket::fs::FileServer; + /// + /// #[launch] + /// fn rocket() -> _ { + /// rocket::build().mount("/static", FileServer::from("/www/public").rank(30)) + /// } + /// ``` + pub fn from>(path: P) -> Self { + FileServer::new(path, Options::default()) + } + + /// Constructs a new `FileServer` that serves files from the file system + /// `path` with `options` enabled. By default, the handler's routes have a + /// rank of `10`. To choose a different rank, use [`FileServer::rank()`]. + /// + /// # Panics + /// + /// Panics if `path` does not exist or is not a directory. + /// + /// # Example + /// + /// Serve the static files in the `/www/public` local directory on path + /// `/static` without serving index files or dot files. Additionally, serve + /// the same files on `/pub` with a route rank of -1 while also serving + /// index files and dot files. + /// + /// ```rust,no_run + /// # #[macro_use] extern crate rocket; + /// use rocket::fs::{FileServer, Options}; + /// + /// #[launch] + /// fn rocket() -> _ { + /// let options = Options::Index | Options::DotFiles; + /// rocket::build() + /// .mount("/static", FileServer::from("/www/public")) + /// .mount("/pub", FileServer::new("/www/public", options).rank(-1)) + /// } + /// ``` + pub fn new>(path: P, options: Options) -> Self { + use crate::yansi::Paint; + + let path = path.as_ref(); + if !path.is_dir() { + error!("`FileServer` supplied with invalid path"); + info_!("'{}' is not a directory", Paint::white(path.display())); + panic!("refusing to continue due to invalid static files path"); + } + + FileServer { root: path.into(), options, rank: Self::DEFAULT_RANK } + } + + /// Sets the rank for generated routes to `rank`. + /// + /// # Example + /// + /// ```rust,no_run + /// use rocket::fs::{FileServer, Options}; + /// + /// // A `FileServer` created with `from()` with routes of rank `3`. + /// FileServer::from("/public").rank(3); + /// + /// // A `FileServer` created with `new()` with routes of rank `-15`. + /// FileServer::new("/public", Options::Index).rank(-15); + /// ``` + pub fn rank(mut self, rank: isize) -> Self { + self.rank = rank; + self + } +} + +impl Into> for FileServer { + fn into(self) -> Vec { + let source = figment::Source::File(self.root.clone()); + let mut route = Route::ranked(self.rank, Method::Get, "/", self); + route.name = Some(format!("FileServer: {}/", source).into()); + vec![route] + } +} + +#[crate::async_trait] +impl Handler for FileServer { + async fn handle<'r>(&self, req: &'r Request<'_>, data: Data) -> Outcome<'r> { + use crate::http::uri::fmt::Path; + + // Get the segments as a `PathBuf`, allowing dotfiles requested. + let options = self.options; + let allow_dotfiles = options.contains(Options::DotFiles); + let path = req.segments::>(0..).ok() + .and_then(|segments| segments.to_path_buf(allow_dotfiles).ok()) + .map(|path| self.root.join(path)); + + match path { + Some(p) if p.is_dir() => { + // Normalize '/a/b/foo' to '/a/b/foo/'. + if options.contains(Options::NormalizeDirs) && !req.uri().path().ends_with('/') { + let normal = req.uri().map_path(|p| format!("{}/", p)) + .expect("adding a trailing slash to a known good path => valid path") + .into_owned(); + + return Outcome::from_or_forward(req, data, Redirect::permanent(normal)); + } + + if !options.contains(Options::Index) { + return Outcome::forward(data); + } + + let index = NamedFile::open(p.join("index.html")).await.ok(); + Outcome::from_or_forward(req, data, index) + }, + Some(p) => Outcome::from_or_forward(req, data, NamedFile::open(p).await.ok()), + None => Outcome::forward(data), + } + } +} + +/// A bitset representing configurable options for [`FileServer`]. /// /// The valid options are: /// @@ -85,7 +241,7 @@ impl Options { /// Respond to requests for a directory with the `index.html` file in that /// directory, if it exists. /// - /// When enabled, [`StaticFiles`] will respond to requests for a directory + /// When enabled, [`FileServer`] will respond to requests for a directory /// `/foo` or `/foo/` with the file at `${root}/foo/index.html` if it /// exists. When disabled, requests to directories will always forward. /// @@ -94,7 +250,7 @@ impl Options { /// Allow requests to dotfiles. /// - /// When enabled, [`StaticFiles`] will respond to requests for files or + /// When enabled, [`FileServer`] will respond to requests for files or /// directories beginning with `.`. When disabled, any dotfiles will be /// treated as missing. /// @@ -104,7 +260,7 @@ impl Options { /// Normalizes directory requests by redirecting requests to directory paths /// without a trailing slash to ones with a trailing slash. /// - /// When enabled, the [`StaticFiles`] handler will respond to requests for a + /// When enabled, the [`FileServer`] handler will respond to requests for a /// directory without a trailing `/` with a permanent redirect (308) to the /// same path with a trailing `/`. This ensures relative URLs within any /// document served from that directory will be interpreted relative to that @@ -123,7 +279,7 @@ impl Options { /// └── index.html /// ``` /// - /// ...with `StaticFiles::from("static")`, both requests to `/foo` and + /// ...with `FileServer::from("static")`, both requests to `/foo` and /// `/foo/` will serve `static/foo/index.html`. If `index.html` references /// `cat.jpeg` as a relative URL, the browser will request `/cat.jpeg` /// (`static/cat.jpeg`) when the request for `/foo` was handled and @@ -139,7 +295,7 @@ impl Options { /// # Example /// /// ```rust - /// use rocket_contrib::serve::Options; + /// use rocket::fs::Options; /// /// let index_request = Options::Index | Options::DotFiles; /// assert!(index_request.contains(Options::Index)); @@ -175,217 +331,52 @@ impl std::ops::BitOr for Options { } } -/// Custom handler for serving static files. -/// -/// This handler makes it simple to serve static files from a directory on the -/// local file system. To use it, construct a `StaticFiles` using either -/// [`StaticFiles::from()`] or [`StaticFiles::new()`] then simply `mount` the -/// handler at a desired path. When mounted, the handler will generate route(s) -/// that serve the desired static files. If a requested file is not found, the -/// routes _forward_ the incoming request. The default rank of the generated -/// routes is `10`. To customize route ranking, use the [`StaticFiles::rank()`] -/// method. -/// -/// # Options -/// -/// The handler's functionality can be customized by passing an [`Options`] to -/// [`StaticFiles::new()`]. -/// -/// # Example -/// -/// To serve files from the `/static` directory on the local file system at the -/// `/public` path, allowing `index.html` files to be used to respond to -/// requests for a directory (the default), you might write the following: -/// -/// ```rust,no_run -/// # #[macro_use] extern crate rocket; -/// # extern crate rocket_contrib; -/// use rocket_contrib::serve::StaticFiles; -/// -/// #[launch] -/// fn rocket() -> _ { -/// rocket::build().mount("/public", StaticFiles::from("/static")) -/// } -/// ``` -/// -/// With this, requests for files at `/public/` will be handled by -/// returning the contents of `/static/`. Requests for _directories_ at -/// `/public/` will be handled by returning the contents of -/// `/static//index.html`. -/// -/// ## Relative Paths -/// -/// In the example above, `/static` is an absolute path. If your static files -/// are stored relative to your crate and your project is managed by Rocket, use -/// the [`crate_relative!`] macro to obtain a path that is relative to your -/// crate's root. For example, to serve files in the `static` subdirectory of -/// your crate at `/`, you might write: -/// -/// ```rust,no_run -/// # #[macro_use] extern crate rocket; -/// # extern crate rocket_contrib; -/// use rocket_contrib::serve::{StaticFiles, crate_relative}; -/// -/// #[launch] -/// fn rocket() -> _ { -/// rocket::build().mount("/", StaticFiles::from(crate_relative!("static"))) -/// } -/// ``` -#[derive(Clone)] -pub struct StaticFiles { - root: PathBuf, - options: Options, - rank: isize, -} - -impl StaticFiles { - /// The default rank use by `StaticFiles` routes. - const DEFAULT_RANK: isize = 10; - - /// Constructs a new `StaticFiles` that serves files from the file system - /// `path`. By default, [`Options::Index`] is set, and the generated routes - /// have a rank of `10`. To serve static files with other options, use - /// [`StaticFiles::new()`]. To choose a different rank for generated routes, - /// use [`StaticFiles::rank()`]. +crate::export! { + /// Generates a crate-relative version of a path. /// - /// # Panics + /// This macro is primarily intended for use with [`FileServer`] to serve + /// files from a path relative to the crate root. /// - /// Panics if `path` does not exist or is not a directory. + /// The macro accepts one parameter, `$path`, an absolute or (preferably) + /// relative path. It returns a path as an `&'static str` prefixed with the + /// path to the crate root. Use `Path::new(relative!($path))` to retrieve an + /// `&'static Path`. /// /// # Example /// - /// Serve the static files in the `/www/public` local directory on path - /// `/static`. + /// Serve files from the crate-relative `static/` directory: /// - /// ```rust,no_run + /// ```rust /// # #[macro_use] extern crate rocket; - /// # extern crate rocket_contrib; - /// use rocket_contrib::serve::StaticFiles; + /// use rocket::fs::{FileServer, relative}; /// /// #[launch] /// fn rocket() -> _ { - /// rocket::build().mount("/static", StaticFiles::from("/www/public")) + /// rocket::build().mount("/", FileServer::from(relative!("static"))) /// } /// ``` /// - /// Exactly as before, but set the rank for generated routes to `30`. + /// Path equivalences: /// - /// ```rust,no_run - /// # #[macro_use] extern crate rocket; - /// # extern crate rocket_contrib; - /// use rocket_contrib::serve::StaticFiles; + /// ```rust + /// use std::path::Path; /// - /// #[launch] - /// fn rocket() -> _ { - /// rocket::build().mount("/static", StaticFiles::from("/www/public").rank(30)) - /// } + /// use rocket::fs::relative; + /// + /// let manual = Path::new(env!("CARGO_MANIFEST_DIR")).join("static"); + /// let automatic_1 = Path::new(relative!("static")); + /// let automatic_2 = Path::new(relative!("/static")); + /// assert_eq!(manual, automatic_1); + /// assert_eq!(automatic_1, automatic_2); /// ``` - pub fn from>(path: P) -> Self { - StaticFiles::new(path, Options::default()) - } - - /// Constructs a new `StaticFiles` that serves files from the file system - /// `path` with `options` enabled. By default, the handler's routes have a - /// rank of `10`. To choose a different rank, use [`StaticFiles::rank()`]. /// - /// # Panics - /// - /// Panics if `path` does not exist or is not a directory. - /// - /// # Example - /// - /// Serve the static files in the `/www/public` local directory on path - /// `/static` without serving index files or dot files. Additionally, serve - /// the same files on `/pub` with a route rank of -1 while also serving - /// index files and dot files. - /// - /// ```rust,no_run - /// # #[macro_use] extern crate rocket; - /// # extern crate rocket_contrib; - /// use rocket_contrib::serve::{StaticFiles, Options}; - /// - /// #[launch] - /// fn rocket() -> _ { - /// let options = Options::Index | Options::DotFiles; - /// rocket::build() - /// .mount("/static", StaticFiles::from("/www/public")) - /// .mount("/pub", StaticFiles::new("/www/public", options).rank(-1)) - /// } - /// ``` - pub fn new>(path: P, options: Options) -> Self { - use rocket::yansi::Paint; - - let path = path.as_ref(); - if !path.is_dir() { - error!("`StaticFiles` supplied with invalid path"); - info_!("'{}' is not a directory", Paint::white(path.display())); - panic!("refusing to continue due to invalid static files path"); - } - - StaticFiles { root: path.into(), options, rank: Self::DEFAULT_RANK } - } - - /// Sets the rank for generated routes to `rank`. - /// - /// # Example - /// - /// ```rust,no_run - /// # extern crate rocket_contrib; - /// use rocket_contrib::serve::{StaticFiles, Options}; - /// - /// // A `StaticFiles` created with `from()` with routes of rank `3`. - /// StaticFiles::from("/public").rank(3); - /// - /// // A `StaticFiles` created with `new()` with routes of rank `-15`. - /// StaticFiles::new("/public", Options::Index).rank(-15); - /// ``` - pub fn rank(mut self, rank: isize) -> Self { - self.rank = rank; - self - } -} - -impl Into> for StaticFiles { - fn into(self) -> Vec { - let source = rocket::figment::Source::File(self.root.clone()); - let mut route = Route::ranked(self.rank, Method::Get, "/", self); - route.name = Some(format!("StaticFiles: {}/", source).into()); - vec![route] - } -} - -#[rocket::async_trait] -impl Handler for StaticFiles { - async fn handle<'r>(&self, req: &'r Request<'_>, data: Data) -> Outcome<'r> { - use rocket::http::uri::fmt::Path; - - // Get the segments as a `PathBuf`, allowing dotfiles requested. - let options = self.options; - let allow_dotfiles = options.contains(Options::DotFiles); - let path = req.segments::>(0..).ok() - .and_then(|segments| segments.to_path_buf(allow_dotfiles).ok()) - .map(|path| self.root.join(path)); - - match path { - Some(p) if p.is_dir() => { - // Normalize '/a/b/foo' to '/a/b/foo/'. - if options.contains(Options::NormalizeDirs) && !req.uri().path().ends_with('/') { - let normal = req.uri().map_path(|p| format!("{}/", p)) - .expect("adding a trailing slash to a known good path => valid path") - .into_owned(); - - return Outcome::from_or_forward(req, data, Redirect::permanent(normal)); - } - - if !options.contains(Options::Index) { - return Outcome::forward(data); - } - - let index = NamedFile::open(p.join("index.html")).await.ok(); - Outcome::from_or_forward(req, data, index) - }, - Some(p) => Outcome::from_or_forward(req, data, NamedFile::open(p).await.ok()), - None => Outcome::forward(data), - } + macro_rules! relative { + ($path:expr) => { + if cfg!(windows) { + concat!(env!("CARGO_MANIFEST_DIR"), "\\", $path) + } else { + concat!(env!("CARGO_MANIFEST_DIR"), "/", $path) + } + }; } } diff --git a/core/lib/src/lib.rs b/core/lib/src/lib.rs index 27a641e9..08252409 100644 --- a/core/lib/src/lib.rs +++ b/core/lib/src/lib.rs @@ -120,6 +120,7 @@ pub mod catcher; pub mod route; pub mod serde; pub mod shield; +pub mod fs; // Reexport of HTTP everything. pub mod http { diff --git a/core/lib/tests/file_server.rs b/core/lib/tests/file_server.rs new file mode 100644 index 00000000..1271d6ef --- /dev/null +++ b/core/lib/tests/file_server.rs @@ -0,0 +1,188 @@ +use std::{io::Read, fs::File}; +use std::path::Path; + +use rocket::{Rocket, Route, Build}; +use rocket::http::Status; +use rocket::local::blocking::Client; +use rocket::fs::{FileServer, Options, relative}; + +fn static_root() -> &'static Path { + Path::new(relative!("/tests/static")) +} + +fn rocket() -> Rocket { + let root = static_root(); + rocket::build() + .mount("/default", FileServer::from(&root)) + .mount("/no_index", FileServer::new(&root, Options::None)) + .mount("/dots", FileServer::new(&root, Options::DotFiles)) + .mount("/index", FileServer::new(&root, Options::Index)) + .mount("/both", FileServer::new(&root, Options::DotFiles | Options::Index)) + .mount("/redir", FileServer::new(&root, Options::NormalizeDirs)) + .mount("/redir_index", FileServer::new(&root, Options::NormalizeDirs | Options::Index)) +} + +static REGULAR_FILES: &[&str] = &[ + "index.html", + "inner/goodbye", + "inner/index.html", + "other/hello.txt", +]; + +static HIDDEN_FILES: &[&str] = &[ + ".hidden", + "inner/.hideme", +]; + +static INDEXED_DIRECTORIES: &[&str] = &[ + "", + "inner/", +]; + +fn assert_file(client: &Client, prefix: &str, path: &str, exists: bool) { + let full_path = format!("/{}/{}", prefix, path); + let response = client.get(full_path).dispatch(); + if exists { + assert_eq!(response.status(), Status::Ok); + + let mut path = static_root().join(path); + if path.is_dir() { + path = path.join("index.html"); + } + + let mut file = File::open(path).expect("open file"); + let mut expected_contents = String::new(); + file.read_to_string(&mut expected_contents).expect("read file"); + assert_eq!(response.into_string(), Some(expected_contents)); + } else { + assert_eq!(response.status(), Status::NotFound); + } +} + +fn assert_all(client: &Client, prefix: &str, paths: &[&str], exist: bool) { + for path in paths.iter() { + assert_file(client, prefix, path, exist); + } +} + +#[test] +fn test_static_no_index() { + let client = Client::debug(rocket()).expect("valid rocket"); + assert_all(&client, "no_index", REGULAR_FILES, true); + assert_all(&client, "no_index", HIDDEN_FILES, false); + assert_all(&client, "no_index", INDEXED_DIRECTORIES, false); +} + +#[test] +fn test_static_hidden() { + let client = Client::debug(rocket()).expect("valid rocket"); + assert_all(&client, "dots", REGULAR_FILES, true); + assert_all(&client, "dots", HIDDEN_FILES, true); + assert_all(&client, "dots", INDEXED_DIRECTORIES, false); +} + +#[test] +fn test_static_index() { + let client = Client::debug(rocket()).expect("valid rocket"); + assert_all(&client, "index", REGULAR_FILES, true); + assert_all(&client, "index", HIDDEN_FILES, false); + assert_all(&client, "index", INDEXED_DIRECTORIES, true); + + assert_all(&client, "default", REGULAR_FILES, true); + assert_all(&client, "default", HIDDEN_FILES, false); + assert_all(&client, "default", INDEXED_DIRECTORIES, true); +} + +#[test] +fn test_static_all() { + let client = Client::debug(rocket()).expect("valid rocket"); + assert_all(&client, "both", REGULAR_FILES, true); + assert_all(&client, "both", HIDDEN_FILES, true); + assert_all(&client, "both", INDEXED_DIRECTORIES, true); +} + +#[test] +fn test_ranking() { + let root = static_root(); + for rank in -128..128 { + let a = FileServer::new(&root, Options::None).rank(rank); + let b = FileServer::from(&root).rank(rank); + + for handler in vec![a, b] { + let routes: Vec = handler.into(); + assert!(routes.iter().all(|route| route.rank == rank), "{}", rank); + } + } +} + +#[test] +fn test_forwarding() { + use rocket::{get, routes}; + + #[get("/", rank = 20)] + fn catch_one(value: String) -> String { value } + + #[get("//", rank = 20)] + fn catch_two(a: &str, b: &str) -> String { format!("{}/{}", a, b) } + + let rocket = rocket().mount("/default", routes![catch_one, catch_two]); + let client = Client::debug(rocket).expect("valid rocket"); + + let response = client.get("/default/ireallydontexist").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.into_string().unwrap(), "ireallydontexist"); + + let response = client.get("/default/idont/exist").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.into_string().unwrap(), "idont/exist"); + + assert_all(&client, "both", REGULAR_FILES, true); + assert_all(&client, "both", HIDDEN_FILES, true); + assert_all(&client, "both", INDEXED_DIRECTORIES, true); +} + +#[test] +fn test_redirection() { + let client = Client::debug(rocket()).expect("valid rocket"); + + // Redirection only happens if enabled, and doesn't affect index behaviour. + let response = client.get("/no_index/inner").dispatch(); + assert_eq!(response.status(), Status::NotFound); + + let response = client.get("/index/inner").dispatch(); + assert_eq!(response.status(), Status::Ok); + + let response = client.get("/redir/inner").dispatch(); + assert_eq!(response.status(), Status::PermanentRedirect); + assert_eq!(response.headers().get("Location").next(), Some("/redir/inner/")); + + let response = client.get("/redir/inner?foo=bar").dispatch(); + assert_eq!(response.status(), Status::PermanentRedirect); + assert_eq!(response.headers().get("Location").next(), Some("/redir/inner/?foo=bar")); + + let response = client.get("/redir_index/inner").dispatch(); + assert_eq!(response.status(), Status::PermanentRedirect); + assert_eq!(response.headers().get("Location").next(), Some("/redir_index/inner/")); + + // Paths with trailing slash are unaffected. + let response = client.get("/redir/inner/").dispatch(); + assert_eq!(response.status(), Status::NotFound); + + let response = client.get("/redir_index/inner/").dispatch(); + assert_eq!(response.status(), Status::Ok); + + // Root of route is also redirected. + let response = client.get("/no_index").dispatch(); + assert_eq!(response.status(), Status::NotFound); + + let response = client.get("/index").dispatch(); + assert_eq!(response.status(), Status::Ok); + + let response = client.get("/redir").dispatch(); + assert_eq!(response.status(), Status::PermanentRedirect); + assert_eq!(response.headers().get("Location").next(), Some("/redir/")); + + let response = client.get("/redir_index").dispatch(); + assert_eq!(response.status(), Status::PermanentRedirect); + assert_eq!(response.headers().get("Location").next(), Some("/redir_index/")); +} diff --git a/contrib/lib/tests/static/.hidden b/core/lib/tests/static/.hidden similarity index 100% rename from contrib/lib/tests/static/.hidden rename to core/lib/tests/static/.hidden diff --git a/contrib/lib/tests/static/index.html b/core/lib/tests/static/index.html similarity index 100% rename from contrib/lib/tests/static/index.html rename to core/lib/tests/static/index.html diff --git a/contrib/lib/tests/static/inner/.hideme b/core/lib/tests/static/inner/.hideme similarity index 100% rename from contrib/lib/tests/static/inner/.hideme rename to core/lib/tests/static/inner/.hideme diff --git a/contrib/lib/tests/static/inner/goodbye b/core/lib/tests/static/inner/goodbye similarity index 100% rename from contrib/lib/tests/static/inner/goodbye rename to core/lib/tests/static/inner/goodbye diff --git a/contrib/lib/tests/static/inner/index.html b/core/lib/tests/static/inner/index.html similarity index 100% rename from contrib/lib/tests/static/inner/index.html rename to core/lib/tests/static/inner/index.html diff --git a/contrib/lib/tests/static/other/hello.txt b/core/lib/tests/static/other/hello.txt similarity index 100% rename from contrib/lib/tests/static/other/hello.txt rename to core/lib/tests/static/other/hello.txt diff --git a/core/lib/tests/tls-config-from-source-1503.rs b/core/lib/tests/tls-config-from-source-1503.rs index 1aaaee37..b54fe59f 100644 --- a/core/lib/tests/tls-config-from-source-1503.rs +++ b/core/lib/tests/tls-config-from-source-1503.rs @@ -1,4 +1,4 @@ -macro_rules! crate_relative { +macro_rules! relative { ($path:expr) => { std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/", $path)) }; @@ -9,8 +9,8 @@ fn tls_config_from_soruce() { use rocket::config::{Config, TlsConfig}; use rocket::figment::Figment; - let cert_path = crate_relative!("examples/tls/private/cert.pem"); - let key_path = crate_relative!("examples/tls/private/key.pem"); + let cert_path = relative!("examples/tls/private/cert.pem"); + let key_path = relative!("examples/tls/private/key.pem"); let rocket_config = Config { tls: Some(TlsConfig::from_paths(cert_path, key_path)), diff --git a/examples/README.md b/examples/README.md index 1ba45134..0fb2ebcc 100644 --- a/examples/README.md +++ b/examples/README.md @@ -67,8 +67,8 @@ This directory contains projects showcasing Rocket's features. operations. Uses managed state to implement a simple index hit counter. Also uses managed state to store, retrieve, and push/pop from a concurrent queue. - * **[`static-files`](./static-files)** - Uses `contrib` `StaticFiles` serve - static files. Also creates a `second` manual yet safe version. + * **[`static-files`](./static-files)** - Uses `FileServer` to serve static + files. Also creates a `second` manual yet safe version. * **[`templating`](./templating)** - Illustrates using `contrib` `templates` support with identical examples for handlebars and tera. diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs index de0e56bc..a8930f26 100644 --- a/examples/forms/src/main.rs +++ b/examples/forms/src/main.rs @@ -1,10 +1,10 @@ -#[macro_use]extern crate rocket; +#[macro_use] extern crate rocket; use rocket::http::{Status, ContentType}; use rocket::form::{Form, Contextual, FromForm, FromFormField, Context}; use rocket::data::TempFile; +use rocket::fs::{FileServer, relative}; -use rocket_contrib::serve::{StaticFiles, crate_relative}; use rocket_contrib::templates::Template; #[derive(Debug, FromForm)] @@ -85,5 +85,5 @@ fn rocket() -> _ { rocket::build() .mount("/", routes![index, submit]) .attach(Template::fairing()) - .mount("/", StaticFiles::from(crate_relative!("/static"))) + .mount("/", FileServer::from(relative!("/static"))) } diff --git a/examples/static-files/Cargo.toml b/examples/static-files/Cargo.toml index 5e05c0ac..4fab9ba5 100644 --- a/examples/static-files/Cargo.toml +++ b/examples/static-files/Cargo.toml @@ -7,4 +7,3 @@ publish = false [dependencies] rocket = { path = "../../core/lib" } -rocket_contrib = { path = "../../contrib/lib" } diff --git a/examples/static-files/src/main.rs b/examples/static-files/src/main.rs index ad6f2d40..fce5f5b5 100644 --- a/examples/static-files/src/main.rs +++ b/examples/static-files/src/main.rs @@ -1,16 +1,16 @@ #[cfg(test)] mod tests; -use rocket_contrib::serve::{StaticFiles, crate_relative}; +use rocket::fs::{FileServer, relative}; // If we wanted or needed to serve files manually, we'd use `NamedFile`. Always -// prefer to use `StaticFiles`! +// prefer to use `FileServer`! mod manual { use std::path::{PathBuf, Path}; use rocket::response::NamedFile; #[rocket::get("/second/")] pub async fn second(path: PathBuf) -> Option { - let mut path = Path::new(super::crate_relative!("static")).join(path); + let mut path = Path::new(super::relative!("static")).join(path); if path.is_dir() { path.push("index.html"); } @@ -23,5 +23,5 @@ mod manual { fn rocket() -> _ { rocket::build() .mount("/", rocket::routes![manual::second]) - .mount("/", StaticFiles::from(crate_relative!("static"))) + .mount("/", FileServer::from(relative!("static"))) } diff --git a/examples/todo/Cargo.toml b/examples/todo/Cargo.toml index 2ec8fea5..85088301 100644 --- a/examples/todo/Cargo.toml +++ b/examples/todo/Cargo.toml @@ -17,4 +17,4 @@ rand = "0.8" [dependencies.rocket_contrib] path = "../../contrib/lib" default_features = false -features = ["tera_templates", "diesel_sqlite_pool", "serve"] +features = ["tera_templates", "diesel_sqlite_pool"] diff --git a/examples/todo/src/main.rs b/examples/todo/src/main.rs index 6f4f86a2..95b684cc 100644 --- a/examples/todo/src/main.rs +++ b/examples/todo/src/main.rs @@ -13,9 +13,9 @@ use rocket::request::FlashMessage; use rocket::response::{Flash, Redirect}; use rocket::serde::Serialize; use rocket::form::Form; +use rocket::fs::{FileServer, relative}; use rocket_contrib::templates::Template; -use rocket_contrib::serve::{StaticFiles, crate_relative}; use crate::task::{Task, Todo}; @@ -110,7 +110,7 @@ fn rocket() -> _ { .attach(DbConn::fairing()) .attach(Template::fairing()) .attach(AdHoc::on_ignite("Run Migrations", run_migrations)) - .mount("/", StaticFiles::from(crate_relative!("static"))) + .mount("/", FileServer::from(relative!("static"))) .mount("/", routes![index]) .mount("/todo", routes![new, toggle, delete]) } diff --git a/scripts/test.sh b/scripts/test.sh index daac724f..d8b9e9e7 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -62,7 +62,6 @@ function test_contrib() { FEATURES=( tera_templates handlebars_templates - serve diesel_postgres_pool diesel_sqlite_pool diesel_mysql_pool diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index c1c7188b..b0021faa 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -154,13 +154,12 @@ async fn files(file: PathBuf) -> Option { ! tip: Rocket makes it even _easier_ to serve static files! If you need to serve static files from your Rocket application, consider using - the [`StaticFiles`] custom handler from [`rocket_contrib`], which makes it as - simple as: + [`FileServer`], which makes it as simple as: - `rocket.mount("/public", StaticFiles::from("static/"))` + `rocket.mount("/public", FileServer::from("static/"))` [`rocket_contrib`]: @api/rocket_contrib/ -[`StaticFiles`]: @api/rocket_contrib/serve/struct.StaticFiles.html +[`FileServer`]: @api/rocket/fs/struct.FileServer.html [`FromSegments`]: @api/rocket/request/trait.FromSegments.html ## Forwarding