-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement a 'StaticFiles' custom handler in contrib.
Closes #239.
- Loading branch information
1 parent
4098ddd
commit ec130f9
Showing
13 changed files
with
368 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
//! Custom handler and options for static file serving. | ||
|
||
use std::path::{PathBuf, Path}; | ||
|
||
use rocket::{Request, Data, Route}; | ||
use rocket::http::{Method, Status, uri::Segments}; | ||
use rocket::handler::{Handler, Outcome}; | ||
use rocket::response::NamedFile; | ||
use rocket::outcome::IntoOutcome; | ||
|
||
/// A bitset representing configurable options for the [`StaticFiles`] handler. | ||
/// | ||
/// The valid options are: | ||
/// | ||
/// * [`Options::None`] - Return only present, visible files. | ||
/// * [`Options::DotFiles`] - In addition to visible files, return dotfiles. | ||
/// * [`Options::Index`] - Render `index.html` pages for directory requests. | ||
/// | ||
/// Two `Options` structures can be `or`d together to slect two or more options. | ||
/// For instance, to request that both dot files and index pages be returned, | ||
/// use `Options::DotFiles | Options::Index`. | ||
#[derive(Debug, Clone, Copy)] | ||
pub struct Options(u8); | ||
|
||
#[allow(non_upper_case_globals)] | ||
impl Options { | ||
/// `Options` representing the empty set. No dotfiles or index pages are | ||
/// rendered. This is different than the _default_, which enables `Index`. | ||
pub const None: Options = Options(0b0000); | ||
|
||
/// `Options` enabling responding to requests for a directory with the | ||
/// `index.html` file in that directory, if it exists. When this is enabled, | ||
/// the [`StaticFiles`] handler will respond to requests for a directory | ||
/// `/foo` with the file `${root}/foo/index.html` if it exists. This is | ||
/// enabled by default. | ||
pub const Index: Options = Options(0b0001); | ||
|
||
/// `Options` enabling returning dot files. When this is enabled, the | ||
/// [`StaticFiles`] handler will respond to requests for files or | ||
/// directories beginning with `.`. This is _not_ enabled by default. | ||
pub const DotFiles: Options = Options(0b0010); | ||
|
||
/// Returns `true` if `self` is a superset of `other`. In other words, | ||
/// returns `true` if all of the options in `other` are also in `self`. | ||
/// | ||
/// # Example | ||
/// | ||
/// ```rust | ||
/// use rocket_contrib::static_files::Options; | ||
/// | ||
/// let index_request = Options::Index | Options::DotFiles; | ||
/// assert!(index_request.contains(Options::Index)); | ||
/// assert!(index_request.contains(Options::DotFiles)); | ||
/// | ||
/// let index_only = Options::Index; | ||
/// assert!(index_only.contains(Options::Index)); | ||
/// assert!(!index_only.contains(Options::DotFiles)); | ||
/// | ||
/// let dot_only = Options::DotFiles; | ||
/// assert!(dot_only.contains(Options::DotFiles)); | ||
/// assert!(!dot_only.contains(Options::Index)); | ||
/// ``` | ||
#[inline] | ||
pub fn contains(self, other: Options) -> bool { | ||
(other.0 & self.0) == other.0 | ||
} | ||
} | ||
|
||
impl ::std::ops::BitOr for Options { | ||
type Output = Self; | ||
|
||
#[inline(always)] | ||
fn bitor(self, rhs: Self) -> Self { | ||
Options(self.0 | rhs.0) | ||
} | ||
} | ||
|
||
/// 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. | ||
/// | ||
/// # Options | ||
/// | ||
/// The handler's functionality can be customized by passing an [`Options`] to | ||
/// [`StaticFiles::new()`]. | ||
/// | ||
/// # Example | ||
/// | ||
/// To serve files from this 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; | ||
/// # extern crate rocket_contrib; | ||
/// use rocket_contrib::static_files::StaticFiles; | ||
/// | ||
/// fn main() { | ||
/// # if false { | ||
/// rocket::ignite() | ||
/// .mount("/public", StaticFiles::from("/static")) | ||
/// .launch(); | ||
/// # } | ||
/// } | ||
/// ``` | ||
/// | ||
/// 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`. | ||
/// | ||
/// 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 | ||
/// server is started in the crate's root directory or use the | ||
/// `CARGO_MANIFEST_DIR` to create an absolute path relative to your crate root. | ||
/// For example, to serve files in the `static` subdirectory of your crate at | ||
/// `/`, you might write: | ||
/// | ||
/// ```rust | ||
/// # extern crate rocket; | ||
/// # extern crate rocket_contrib; | ||
/// use rocket_contrib::static_files::StaticFiles; | ||
/// | ||
/// fn main() { | ||
/// # if false { | ||
/// rocket::ignite() | ||
/// .mount("/", StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/static"))) | ||
/// .launch(); | ||
/// # } | ||
/// } | ||
/// ``` | ||
#[derive(Clone)] | ||
pub struct StaticFiles { | ||
root: PathBuf, | ||
options: Options, | ||
} | ||
|
||
impl StaticFiles { | ||
/// Constructs a new `StaticFiles` that serves files from the file system | ||
/// `path`. By default, [`Options::Index`] is enabled. To serve static files | ||
/// with other options, use [`StaticFiles::new()`]. | ||
/// | ||
/// # Example | ||
/// | ||
/// Serve the static files in the `/www/public` local directory on path | ||
/// `/static`. | ||
/// | ||
/// ```rust | ||
/// # extern crate rocket; | ||
/// # extern crate rocket_contrib; | ||
/// use rocket_contrib::static_files::StaticFiles; | ||
/// | ||
/// fn main() { | ||
/// # if false { | ||
/// rocket::ignite() | ||
/// .mount("/static", StaticFiles::from("/www/public")) | ||
/// .launch(); | ||
/// # } | ||
/// } | ||
/// ``` | ||
pub fn from<P: AsRef<Path>>(path: P) -> Self { | ||
StaticFiles::new(path, Options::Index) | ||
} | ||
|
||
/// Constructs a new `StaticFiles` that serves files from the file system | ||
/// `path` with `options` enabled. | ||
/// | ||
/// # 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` while also seriving index files and dot files. | ||
/// | ||
/// ```rust | ||
/// # extern crate rocket; | ||
/// # extern crate rocket_contrib; | ||
/// use rocket_contrib::static_files::{StaticFiles, Options}; | ||
/// | ||
/// fn main() { | ||
/// # if false { | ||
/// let options = Options::Index | Options::DotFiles; | ||
/// rocket::ignite() | ||
/// .mount("/static", StaticFiles::from("/www/public")) | ||
/// .mount("/pub", StaticFiles::new("/www/public", options)) | ||
/// .launch(); | ||
/// # } | ||
/// } | ||
/// ``` | ||
pub fn new<P: AsRef<Path>>(path: P, options: Options) -> Self { | ||
StaticFiles { root: path.as_ref().into(), options } | ||
} | ||
} | ||
|
||
impl Into<Vec<Route>> for StaticFiles { | ||
fn into(self) -> Vec<Route> { | ||
let non_index = Route::ranked(10, Method::Get, "/<path..>", self.clone()); | ||
if self.options.contains(Options::Index) { | ||
let index = Route::ranked(10, Method::Get, "/", self); | ||
vec![index, non_index] | ||
} else { | ||
vec![non_index] | ||
} | ||
} | ||
} | ||
|
||
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> { | ||
if !opt.contains(Options::Index) { | ||
return Outcome::failure(Status::NotFound); | ||
} | ||
|
||
Outcome::from(r, NamedFile::open(path.join("index.html")).ok()) | ||
} | ||
|
||
// If this is not the route with segments, handle it only if the user | ||
// requested a handling of index files. | ||
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); | ||
} | ||
|
||
// Otherwise, we're handling segments. Get the segments as a `PathBuf`, | ||
// only allowing dotfiles if the user allowed it. | ||
let allow_dotfiles = self.options.contains(Options::DotFiles); | ||
let path = req.get_segments::<Segments>(0).ok() | ||
.and_then(|segments| segments.into_path_buf(allow_dotfiles).ok()) | ||
.map(|path| self.root.join(path)) | ||
.into_outcome(Status::NotFound)?; | ||
|
||
if path.is_dir() { | ||
handle_index(self.options, req, &path) | ||
} else { | ||
Outcome::from(req, NamedFile::open(&path).ok()) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Peek-a-boo. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Just a file here: index.html. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Oh no! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Thanks for coming! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Inner index. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Hi! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
#![feature(plugin, decl_macro)] | ||
#![plugin(rocket_codegen)] | ||
|
||
extern crate rocket; | ||
extern crate rocket_contrib; | ||
|
||
#[cfg(feature = "static_files")] | ||
mod static_files_tests { | ||
use std::{io::Read, fs::File}; | ||
use std::path::{Path, PathBuf}; | ||
|
||
use rocket::{self, Rocket}; | ||
use rocket_contrib::static_files::{StaticFiles, Options}; | ||
use rocket::http::Status; | ||
use rocket::local::Client; | ||
|
||
fn static_root() -> PathBuf { | ||
Path::new(env!("CARGO_MANIFEST_DIR")) | ||
.join("tests") | ||
.join("static") | ||
} | ||
|
||
fn rocket() -> Rocket { | ||
let root = static_root(); | ||
rocket::ignite() | ||
.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)) | ||
} | ||
|
||
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!("/{}", Path::new(prefix).join(path).display()); | ||
let mut 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.body_string(), Some(expected_contents)); | ||
} else { | ||
assert_eq!(response.status(), Status::NotFound); | ||
} | ||
} | ||
|
||
fn assert_all(client: &Client, prefix: &str, paths: &[&str], exist: bool) { | ||
paths.iter().for_each(|path| assert_file(client, prefix, path, exist)) | ||
} | ||
|
||
#[test] | ||
fn test_static_no_index() { | ||
let client = Client::new(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::new(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::new(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::new(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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.