Graduate 'serve' into core as 'fs', 'FileServer'.

This completes the graduation of stable 'contrib' features to 'core'.

Closes #1107.
This commit is contained in:
Sergio Benitez 2021-05-22 10:55:59 -07:00
parent a78814f1c5
commit b1d05d20ac
22 changed files with 453 additions and 465 deletions

View File

@ -20,10 +20,9 @@ databases = [
] ]
# User-facing features. # User-facing features.
default = ["serve"] default = []
tera_templates = ["tera", "templates"] tera_templates = ["tera", "templates"]
handlebars_templates = ["handlebars", "templates"] handlebars_templates = ["handlebars", "templates"]
serve = []
compression = ["brotli_compression", "gzip_compression"] compression = ["brotli_compression", "gzip_compression"]
brotli_compression = ["brotli"] brotli_compression = ["brotli"]
gzip_compression = ["flate2"] gzip_compression = ["flate2"]

View File

@ -16,7 +16,6 @@
//! common modules exposed by default. The present feature list is below, with //! common modules exposed by default. The present feature list is below, with
//! an asterisk next to the features that are enabled by default: //! an asterisk next to the features that are enabled by default:
//! //!
//! * [serve*](serve) - Static File Serving
//! * [handlebars_templates](templates) - Handlebars Templating //! * [handlebars_templates](templates) - Handlebars Templating
//! * [tera_templates](templates) - Tera Templating //! * [tera_templates](templates) - Tera Templating
//! * [${database}_pool](databases) - Database Configuration and Pooling //! * [${database}_pool](databases) - Database Configuration and Pooling
@ -39,7 +38,6 @@
#[allow(unused_imports)] #[macro_use] extern crate rocket; #[allow(unused_imports)] #[macro_use] extern crate rocket;
#[cfg(feature="serve")] pub mod serve;
#[cfg(feature="templates")] pub mod templates; #[cfg(feature="templates")] pub mod templates;
#[cfg(feature="databases")] pub mod databases; #[cfg(feature="databases")] pub mod databases;
// TODO.async: Migrate compression, reenable this, tests, and add to docs. // TODO.async: Migrate compression, reenable this, tests, and add to docs.

View File

@ -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<Build> {
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<Route> = handler.into();
assert!(routes.iter().all(|route| route.rank == rank), "{}", rank);
}
}
}
#[test]
fn test_forwarding() {
use rocket::{get, routes};
#[get("/<value>", rank = 20)]
fn catch_one(value: String) -> String { value }
#[get("/<a>/<b>", 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/"));
}
}

6
core/lib/src/fs/mod.rs Normal file
View File

@ -0,0 +1,6 @@
//! File serving, file accepting, and file system types.
mod server;
pub use server::*;
pub use server::relative;

View File

@ -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 std::path::{PathBuf, Path};
use rocket::{Request, Data}; use crate::{Request, Data};
use rocket::http::{Method, uri::Segments, ext::IntoOwned}; use crate::http::{Method, uri::Segments, ext::IntoOwned};
use rocket::response::{NamedFile, Redirect}; use crate::response::{NamedFile, Redirect};
use rocket::route::{Route, Handler, Outcome}; 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 /// This handler makes it simple to serve static files from a directory on the
/// from a path relative to the crate root. The macro accepts one parameter, /// local file system. To use it, construct a `FileServer` using either
/// `$path`, an absolute or, preferably, relative path. It returns a path (an /// [`FileServer::from()`] or [`FileServer::new()`] then simply `mount` the
/// `&'static str`) prefixed with the path to the crate root. Use `Path::new()` /// handler at a desired path. When mounted, the handler will generate route(s)
/// to retrieve an `&'static Path`. /// 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` /// # Options
/// documentation](`StaticFiles`#relative-paths) for an example. ///
/// The handler's functionality can be customized by passing an [`Options`] to
/// [`FileServer::new()`].
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// To serve files from the `/static` directory on the local file system at the
/// use std::path::Path; /// `/public` path, allowing `index.html` files to be used to respond to
/// use rocket_contrib::serve::crate_relative; /// requests for a directory (the default), you might write the following:
/// ///
/// let manual = Path::new(env!("CARGO_MANIFEST_DIR")).join("static"); /// ```rust,no_run
/// let automatic_1 = Path::new(crate_relative!("static")); /// # #[macro_use] extern crate rocket;
/// let automatic_2 = Path::new(crate_relative!("/static")); /// use rocket::fs::FileServer;
/// assert_eq!(manual, automatic_1); ///
/// assert_eq!(automatic_1, automatic_2); /// #[launch]
/// fn rocket() -> _ {
/// rocket::build().mount("/public", FileServer::from("/static"))
/// }
/// ``` /// ```
#[macro_export] ///
macro_rules! crate_relative { /// With this, requests for files at `/public/<path..>` will be handled by
($path:expr) => { /// returning the contents of `/static/<path..>`. Requests for _directories_ at
if cfg!(windows) { /// `/public/<directory>` will be handled by returning the contents of
concat!(env!("CARGO_MANIFEST_DIR"), "\\", $path) /// `/static/<directory>/index.html`.
} else { ///
concat!(env!("CARGO_MANIFEST_DIR"), "/", $path) /// ## 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)] impl FileServer {
pub use crate_relative; /// 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<P: AsRef<Path>>(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<P: AsRef<Path>>(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<Vec<Route>> for FileServer {
fn into(self) -> Vec<Route> {
let source = figment::Source::File(self.root.clone());
let mut route = Route::ranked(self.rank, Method::Get, "/<path..>", 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::<Segments<'_, Path>>(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: /// The valid options are:
/// ///
@ -85,7 +241,7 @@ impl Options {
/// Respond to requests for a directory with the `index.html` file in that /// Respond to requests for a directory with the `index.html` file in that
/// directory, if it exists. /// 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 /// `/foo` or `/foo/` with the file at `${root}/foo/index.html` if it
/// exists. When disabled, requests to directories will always forward. /// exists. When disabled, requests to directories will always forward.
/// ///
@ -94,7 +250,7 @@ impl Options {
/// Allow requests to dotfiles. /// 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 /// directories beginning with `.`. When disabled, any dotfiles will be
/// treated as missing. /// treated as missing.
/// ///
@ -104,7 +260,7 @@ impl Options {
/// Normalizes directory requests by redirecting requests to directory paths /// Normalizes directory requests by redirecting requests to directory paths
/// without a trailing slash to ones with a trailing slash. /// 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 /// directory without a trailing `/` with a permanent redirect (308) to the
/// same path with a trailing `/`. This ensures relative URLs within any /// same path with a trailing `/`. This ensures relative URLs within any
/// document served from that directory will be interpreted relative to that /// document served from that directory will be interpreted relative to that
@ -123,7 +279,7 @@ impl Options {
/// └── index.html /// └── 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 /// `/foo/` will serve `static/foo/index.html`. If `index.html` references
/// `cat.jpeg` as a relative URL, the browser will request `/cat.jpeg` /// `cat.jpeg` as a relative URL, the browser will request `/cat.jpeg`
/// (`static/cat.jpeg`) when the request for `/foo` was handled and /// (`static/cat.jpeg`) when the request for `/foo` was handled and
@ -139,7 +295,7 @@ impl Options {
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// use rocket_contrib::serve::Options; /// use rocket::fs::Options;
/// ///
/// let index_request = Options::Index | Options::DotFiles; /// let index_request = Options::Index | Options::DotFiles;
/// assert!(index_request.contains(Options::Index)); /// assert!(index_request.contains(Options::Index));
@ -175,217 +331,52 @@ impl std::ops::BitOr for Options {
} }
} }
/// Custom handler for serving static files. crate::export! {
/// Generates a crate-relative version of a path.
/// ///
/// This handler makes it simple to serve static files from a directory on the /// This macro is primarily intended for use with [`FileServer`] to serve
/// local file system. To use it, construct a `StaticFiles` using either /// files from a path relative to the crate root.
/// [`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 macro accepts one parameter, `$path`, an absolute or (preferably)
/// /// relative path. It returns a path as an `&'static str` prefixed with the
/// The handler's functionality can be customized by passing an [`Options`] to /// path to the crate root. Use `Path::new(relative!($path))` to retrieve an
/// [`StaticFiles::new()`]. /// `&'static Path`.
/// ///
/// # Example /// # Example
/// ///
/// To serve files from the `/static` directory on the local file system at the /// Serve files from the crate-relative `static/` directory:
/// `/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 /// ```rust
/// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket;
/// # extern crate rocket_contrib; /// use rocket::fs::{FileServer, relative};
/// use rocket_contrib::serve::StaticFiles;
/// ///
/// #[launch] /// #[launch]
/// fn rocket() -> _ { /// fn rocket() -> _ {
/// rocket::build().mount("/public", StaticFiles::from("/static")) /// rocket::build().mount("/", FileServer::from(relative!("static")))
/// } /// }
/// ``` /// ```
/// ///
/// With this, requests for files at `/public/<path..>` will be handled by /// Path equivalences:
/// returning the contents of `/static/<path..>`. Requests for _directories_ at
/// `/public/<directory>` will be handled by returning the contents of
/// `/static/<directory>/index.html`.
/// ///
/// ## Relative Paths /// ```rust
/// use std::path::Path;
/// ///
/// In the example above, `/static` is an absolute path. If your static files /// use rocket::fs::relative;
/// 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 /// let manual = Path::new(env!("CARGO_MANIFEST_DIR")).join("static");
/// # #[macro_use] extern crate rocket; /// let automatic_1 = Path::new(relative!("static"));
/// # extern crate rocket_contrib; /// let automatic_2 = Path::new(relative!("/static"));
/// use rocket_contrib::serve::{StaticFiles, crate_relative}; /// assert_eq!(manual, automatic_1);
/// /// assert_eq!(automatic_1, automatic_2);
/// #[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()`].
///
/// # 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;
/// # extern crate rocket_contrib;
/// use rocket_contrib::serve::StaticFiles;
///
/// #[launch]
/// fn rocket() -> _ {
/// rocket::build().mount("/static", StaticFiles::from("/www/public"))
/// }
/// ``` /// ```
/// ///
/// Exactly as before, but set the rank for generated routes to `30`. macro_rules! relative {
/// ($path:expr) => {
/// ```rust,no_run if cfg!(windows) {
/// # #[macro_use] extern crate rocket; concat!(env!("CARGO_MANIFEST_DIR"), "\\", $path)
/// # extern crate rocket_contrib; } else {
/// use rocket_contrib::serve::StaticFiles; concat!(env!("CARGO_MANIFEST_DIR"), "/", $path)
///
/// #[launch]
/// fn rocket() -> _ {
/// rocket::build().mount("/static", StaticFiles::from("/www/public").rank(30))
/// }
/// ```
pub fn from<P: AsRef<Path>>(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<P: AsRef<Path>>(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<Vec<Route>> for StaticFiles {
fn into(self) -> Vec<Route> {
let source = rocket::figment::Source::File(self.root.clone());
let mut route = Route::ranked(self.rank, Method::Get, "/<path..>", 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::<Segments<'_, Path>>(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),
} }
};
} }
} }

View File

@ -120,6 +120,7 @@ pub mod catcher;
pub mod route; pub mod route;
pub mod serde; pub mod serde;
pub mod shield; pub mod shield;
pub mod fs;
// Reexport of HTTP everything. // Reexport of HTTP everything.
pub mod http { pub mod http {

View File

@ -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<Build> {
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<Route> = handler.into();
assert!(routes.iter().all(|route| route.rank == rank), "{}", rank);
}
}
}
#[test]
fn test_forwarding() {
use rocket::{get, routes};
#[get("/<value>", rank = 20)]
fn catch_one(value: String) -> String { value }
#[get("/<a>/<b>", 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/"));
}

View File

@ -1,4 +1,4 @@
macro_rules! crate_relative { macro_rules! relative {
($path:expr) => { ($path:expr) => {
std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/", $path)) 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::config::{Config, TlsConfig};
use rocket::figment::Figment; use rocket::figment::Figment;
let cert_path = crate_relative!("examples/tls/private/cert.pem"); let cert_path = relative!("examples/tls/private/cert.pem");
let key_path = crate_relative!("examples/tls/private/key.pem"); let key_path = relative!("examples/tls/private/key.pem");
let rocket_config = Config { let rocket_config = Config {
tls: Some(TlsConfig::from_paths(cert_path, key_path)), tls: Some(TlsConfig::from_paths(cert_path, key_path)),

View File

@ -67,8 +67,8 @@ This directory contains projects showcasing Rocket's features.
operations. Uses managed state to implement a simple index hit counter. Also 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. uses managed state to store, retrieve, and push/pop from a concurrent queue.
* **[`static-files`](./static-files)** - Uses `contrib` `StaticFiles` serve * **[`static-files`](./static-files)** - Uses `FileServer` to serve static
static files. Also creates a `second` manual yet safe version. files. Also creates a `second` manual yet safe version.
* **[`templating`](./templating)** - Illustrates using `contrib` `templates` * **[`templating`](./templating)** - Illustrates using `contrib` `templates`
support with identical examples for handlebars and tera. support with identical examples for handlebars and tera.

View File

@ -3,8 +3,8 @@
use rocket::http::{Status, ContentType}; use rocket::http::{Status, ContentType};
use rocket::form::{Form, Contextual, FromForm, FromFormField, Context}; use rocket::form::{Form, Contextual, FromForm, FromFormField, Context};
use rocket::data::TempFile; use rocket::data::TempFile;
use rocket::fs::{FileServer, relative};
use rocket_contrib::serve::{StaticFiles, crate_relative};
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
#[derive(Debug, FromForm)] #[derive(Debug, FromForm)]
@ -85,5 +85,5 @@ fn rocket() -> _ {
rocket::build() rocket::build()
.mount("/", routes![index, submit]) .mount("/", routes![index, submit])
.attach(Template::fairing()) .attach(Template::fairing())
.mount("/", StaticFiles::from(crate_relative!("/static"))) .mount("/", FileServer::from(relative!("/static")))
} }

View File

@ -7,4 +7,3 @@ publish = false
[dependencies] [dependencies]
rocket = { path = "../../core/lib" } rocket = { path = "../../core/lib" }
rocket_contrib = { path = "../../contrib/lib" }

View File

@ -1,16 +1,16 @@
#[cfg(test)] mod tests; #[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 // If we wanted or needed to serve files manually, we'd use `NamedFile`. Always
// prefer to use `StaticFiles`! // prefer to use `FileServer`!
mod manual { mod manual {
use std::path::{PathBuf, Path}; use std::path::{PathBuf, Path};
use rocket::response::NamedFile; use rocket::response::NamedFile;
#[rocket::get("/second/<path..>")] #[rocket::get("/second/<path..>")]
pub async fn second(path: PathBuf) -> Option<NamedFile> { pub async fn second(path: PathBuf) -> Option<NamedFile> {
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() { if path.is_dir() {
path.push("index.html"); path.push("index.html");
} }
@ -23,5 +23,5 @@ mod manual {
fn rocket() -> _ { fn rocket() -> _ {
rocket::build() rocket::build()
.mount("/", rocket::routes![manual::second]) .mount("/", rocket::routes![manual::second])
.mount("/", StaticFiles::from(crate_relative!("static"))) .mount("/", FileServer::from(relative!("static")))
} }

View File

@ -17,4 +17,4 @@ rand = "0.8"
[dependencies.rocket_contrib] [dependencies.rocket_contrib]
path = "../../contrib/lib" path = "../../contrib/lib"
default_features = false default_features = false
features = ["tera_templates", "diesel_sqlite_pool", "serve"] features = ["tera_templates", "diesel_sqlite_pool"]

View File

@ -13,9 +13,9 @@ use rocket::request::FlashMessage;
use rocket::response::{Flash, Redirect}; use rocket::response::{Flash, Redirect};
use rocket::serde::Serialize; use rocket::serde::Serialize;
use rocket::form::Form; use rocket::form::Form;
use rocket::fs::{FileServer, relative};
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
use rocket_contrib::serve::{StaticFiles, crate_relative};
use crate::task::{Task, Todo}; use crate::task::{Task, Todo};
@ -110,7 +110,7 @@ fn rocket() -> _ {
.attach(DbConn::fairing()) .attach(DbConn::fairing())
.attach(Template::fairing()) .attach(Template::fairing())
.attach(AdHoc::on_ignite("Run Migrations", run_migrations)) .attach(AdHoc::on_ignite("Run Migrations", run_migrations))
.mount("/", StaticFiles::from(crate_relative!("static"))) .mount("/", FileServer::from(relative!("static")))
.mount("/", routes![index]) .mount("/", routes![index])
.mount("/todo", routes![new, toggle, delete]) .mount("/todo", routes![new, toggle, delete])
} }

View File

@ -62,7 +62,6 @@ function test_contrib() {
FEATURES=( FEATURES=(
tera_templates tera_templates
handlebars_templates handlebars_templates
serve
diesel_postgres_pool diesel_postgres_pool
diesel_sqlite_pool diesel_sqlite_pool
diesel_mysql_pool diesel_mysql_pool

View File

@ -154,13 +154,12 @@ async fn files(file: PathBuf) -> Option<NamedFile> {
! tip: Rocket makes it even _easier_ to serve static files! ! tip: Rocket makes it even _easier_ to serve static files!
If you need to serve static files from your Rocket application, consider using If you need to serve static files from your Rocket application, consider using
the [`StaticFiles`] custom handler from [`rocket_contrib`], which makes it as [`FileServer`], which makes it as simple as:
simple as:
`rocket.mount("/public", StaticFiles::from("static/"))` `rocket.mount("/public", FileServer::from("static/"))`
[`rocket_contrib`]: @api/rocket_contrib/ [`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 [`FromSegments`]: @api/rocket/request/trait.FromSegments.html
## Forwarding ## Forwarding