mirror of https://github.com/rwf2/Rocket.git
Graduate 'serve' into core as 'fs', 'FileServer'.
This completes the graduation of stable 'contrib' features to 'core'. Closes #1107.
This commit is contained in:
parent
a78814f1c5
commit
b1d05d20ac
|
@ -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"]
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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/"));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
//! File serving, file accepting, and file system types.
|
||||||
|
|
||||||
|
mod server;
|
||||||
|
|
||||||
|
pub use server::*;
|
||||||
|
pub use server::relative;
|
|
@ -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),
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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/"));
|
||||||
|
}
|
|
@ -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)),
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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")))
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,4 +7,3 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = { path = "../../core/lib" }
|
rocket = { path = "../../core/lib" }
|
||||||
rocket_contrib = { path = "../../contrib/lib" }
|
|
||||||
|
|
|
@ -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")))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue