Forward from 'StaticFiles' if a file is not found.

Also adds a 'handler::Outcome::from_or_forward' method for easily
constructing handler outcomes that forward on responder failures.

Fixes #1036.
This commit is contained in:
Sergio Benitez 2019-07-07 23:21:13 -07:00
parent c100a92127
commit 21b10176ee
3 changed files with 77 additions and 23 deletions

View File

@ -17,10 +17,9 @@
use std::path::{PathBuf, Path};
use rocket::{Request, Data, Route};
use rocket::http::{Method, Status, uri::Segments};
use rocket::http::{Method, uri::Segments};
use rocket::handler::{Handler, Outcome};
use rocket::response::NamedFile;
use rocket::outcome::IntoOutcome;
/// A bitset representing configurable options for the [`StaticFiles`] handler.
///
@ -101,19 +100,21 @@ impl std::ops::BitOr for Options {
/// 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.
/// 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()`]. Additionally, the rank of generate routes, which
/// defaults to `10`, can be set via the [`StaticFiles::rank()`] builder method.
/// [`StaticFiles::new()`].
///
/// # Example
///
/// To serve files from the `/static` directory 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:
/// To serve files from the `/static` local file system directory 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
/// # extern crate rocket;
@ -129,10 +130,10 @@ impl std::ops::BitOr for Options {
/// }
/// ```
///
/// With this set-up, 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`.
/// 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`.
///
/// If your static files are stored relative to your crate and your project is
/// managed by Cargo, you should either use a relative path and ensure that your
@ -272,13 +273,14 @@ impl Into<Vec<Route>> for StaticFiles {
}
impl Handler for StaticFiles {
fn handle<'r>(&self, req: &'r Request<'_>, _: Data) -> Outcome<'r> {
fn handle_index<'r>(opt: Options, r: &'r Request<'_>, path: &Path) -> Outcome<'r> {
fn handle<'r>(&self, req: &'r Request<'_>, data: Data) -> Outcome<'r> {
fn handle_dir<'r>(opt: Options, r: &'r Request<'_>, d: Data, path: &Path) -> Outcome<'r> {
if !opt.contains(Options::Index) {
return Outcome::failure(Status::NotFound);
return Outcome::forward(d);
}
Outcome::from(r, NamedFile::open(path.join("index.html")).ok())
let file = NamedFile::open(path.join("index.html")).ok();
Outcome::from_or_forward(r, d, file)
}
// If this is not the route with segments, handle it only if the user
@ -286,7 +288,7 @@ impl Handler for StaticFiles {
let current_route = req.route().expect("route while handling");
let is_segments_route = current_route.uri.path().ends_with(">");
if !is_segments_route {
return handle_index(self.options, req, &self.root);
return handle_dir(self.options, req, data, &self.root);
}
// Otherwise, we're handling segments. Get the segments as a `PathBuf`,
@ -295,13 +297,12 @@ impl Handler for StaticFiles {
let path = req.get_segments::<Segments<'_>>(0)
.and_then(|res| res.ok())
.and_then(|segments| segments.into_path_buf(allow_dotfiles).ok())
.map(|path| self.root.join(path))
.into_outcome(Status::NotFound)?;
.map(|path| self.root.join(path));
if path.is_dir() {
handle_index(self.options, req, &path)
} else {
Outcome::from(req, NamedFile::open(&path).ok())
match &path {
Some(path) if path.is_dir() => handle_dir(self.options, req, data, path),
Some(path) => Outcome::from_or_forward(req, data, NamedFile::open(path).ok()),
None => Outcome::forward(data)
}
}
}

View File

@ -116,4 +116,31 @@ mod static_tests {
}
}
}
#[test]
fn test_forwarding() {
use rocket::http::RawStr;
use rocket::{get, routes};
#[get("/<value>", rank = 20)]
fn catch_one(value: String) -> String { value }
#[get("/<a>/<b>", rank = 20)]
fn catch_two(a: &RawStr, b: &RawStr) -> String { format!("{}/{}", a, b) }
let rocket = rocket().mount("/default", routes![catch_one, catch_two]);
let client = Client::new(rocket).expect("valid rocket");
let mut response = client.get("/default/ireallydontexist").dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.body_string().unwrap(), "ireallydontexist");
let mut response = client.get("/default/idont/exist").dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.body_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);
}
}

View File

@ -206,6 +206,32 @@ impl<'r> Outcome<'r> {
}
}
/// Return the `Outcome` of response to `req` from `responder`.
///
/// If the responder returns `Ok`, an outcome of `Success` is
/// returned with the response. If the responder returns `Err`, an
/// outcome of `Forward` is returned.
///
/// # Example
///
/// ```rust
/// use rocket::{Request, Data};
/// use rocket::handler::Outcome;
///
/// fn str_responder(req: &Request, data: Data) -> Outcome<'static> {
/// Outcome::from_or_forward(req, data, "Hello, world!")
/// }
/// ```
#[inline]
pub fn from_or_forward<T>(req: &Request<'_>, data: Data, responder: T) -> Outcome<'r>
where T: Responder<'r>
{
match responder.respond_to(req) {
Ok(response) => outcome::Outcome::Success(response),
Err(_) => outcome::Outcome::Forward(data)
}
}
/// Return an `Outcome` of `Failure` with the status code `code`. This is
/// equivalent to `Outcome::Failure(code)`.
///