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.
default = ["serve"]
default = []
tera_templates = ["tera", "templates"]
handlebars_templates = ["handlebars", "templates"]
serve = []
compression = ["brotli_compression", "gzip_compression"]
brotli_compression = ["brotli"]
gzip_compression = ["flate2"]

View File

@ -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.

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 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/<path..>` will be handled by
/// 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
///
/// 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<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:
///
@ -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/<path..>` will be handled by
/// 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
///
/// 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<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),
}
macro_rules! relative {
($path:expr) => {
if cfg!(windows) {
concat!(env!("CARGO_MANIFEST_DIR"), "\\", $path)
} else {
concat!(env!("CARGO_MANIFEST_DIR"), "/", $path)
}
};
}
}

View File

@ -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 {

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) => {
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)),

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
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.

View File

@ -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")))
}

View File

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

View File

@ -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/<path..>")]
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() {
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")))
}

View File

@ -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"]

View File

@ -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])
}

View File

@ -62,7 +62,6 @@ function test_contrib() {
FEATURES=(
tera_templates
handlebars_templates
serve
diesel_postgres_pool
diesel_sqlite_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!
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