mirror of https://github.com/rwf2/Rocket.git
parent
4098ddd92f
commit
ec130f96ee
|
@ -11,11 +11,12 @@ keywords = ["rocket", "web", "framework", "contrib", "contributed"]
|
||||||
license = "MIT/Apache-2.0"
|
license = "MIT/Apache-2.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["json"]
|
default = ["json", "static_files"]
|
||||||
json = ["serde", "serde_json"]
|
json = ["serde", "serde_json"]
|
||||||
msgpack = ["serde", "rmp-serde"]
|
msgpack = ["serde", "rmp-serde"]
|
||||||
tera_templates = ["tera", "templates"]
|
tera_templates = ["tera", "templates"]
|
||||||
handlebars_templates = ["handlebars", "templates"]
|
handlebars_templates = ["handlebars", "templates"]
|
||||||
|
static_files = []
|
||||||
|
|
||||||
# Internal use only.
|
# Internal use only.
|
||||||
templates = ["serde", "serde_json", "glob"]
|
templates = ["serde", "serde_json", "glob"]
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
//! an asterisk next to the features that are enabled by default:
|
//! an asterisk next to the features that are enabled by default:
|
||||||
//!
|
//!
|
||||||
//! * [json*](struct.Json.html)
|
//! * [json*](struct.Json.html)
|
||||||
|
//! * [static_files*](static_files)
|
||||||
//! * [msgpack](struct.MsgPack.html)
|
//! * [msgpack](struct.MsgPack.html)
|
||||||
//! * [handlebars_templates](struct.Template.html)
|
//! * [handlebars_templates](struct.Template.html)
|
||||||
//! * [tera_templates](struct.Template.html)
|
//! * [tera_templates](struct.Template.html)
|
||||||
|
@ -82,3 +83,6 @@ mod uuid;
|
||||||
|
|
||||||
#[cfg(feature = "uuid")]
|
#[cfg(feature = "uuid")]
|
||||||
pub use uuid::{Uuid, UuidParseError};
|
pub use uuid::{Uuid, UuidParseError};
|
||||||
|
|
||||||
|
#[cfg(feature = "static_files")]
|
||||||
|
pub mod static_files;
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
Peek-a-boo.
|
|
@ -0,0 +1 @@
|
||||||
|
Just a file here: index.html.
|
|
@ -0,0 +1 @@
|
||||||
|
Oh no!
|
|
@ -0,0 +1 @@
|
||||||
|
Thanks for coming!
|
|
@ -0,0 +1 @@
|
||||||
|
Inner index.
|
|
@ -0,0 +1 @@
|
||||||
|
Hi!
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,3 +7,4 @@ publish = false
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = { path = "../../core/lib" }
|
rocket = { path = "../../core/lib" }
|
||||||
rocket_codegen = { path = "../../core/codegen" }
|
rocket_codegen = { path = "../../core/codegen" }
|
||||||
|
rocket_contrib = { path = "../../contrib/lib" }
|
||||||
|
|
|
@ -2,27 +2,14 @@
|
||||||
#![plugin(rocket_codegen)]
|
#![plugin(rocket_codegen)]
|
||||||
|
|
||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
|
extern crate rocket_contrib;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)] mod tests;
|
||||||
mod tests;
|
|
||||||
|
|
||||||
use std::io;
|
use rocket_contrib::static_files::StaticFiles;
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use rocket::response::NamedFile;
|
|
||||||
|
|
||||||
#[get("/")]
|
|
||||||
fn index() -> io::Result<NamedFile> {
|
|
||||||
NamedFile::open("static/index.html")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/<file..>")]
|
|
||||||
fn files(file: PathBuf) -> Option<NamedFile> {
|
|
||||||
NamedFile::open(Path::new("static/").join(file)).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rocket() -> rocket::Rocket {
|
fn rocket() -> rocket::Rocket {
|
||||||
rocket::ignite().mount("/", routes![index, files])
|
rocket::ignite().mount("/", StaticFiles::from("static"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
|
@ -21,7 +21,7 @@ fn test_query_file<T> (path: &str, file: T, status: Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_file_content(path: &str) -> Vec<u8> {
|
fn read_file_content(path: &str) -> Vec<u8> {
|
||||||
let mut fp = File::open(&path).expect(&format!("Can not open {}", path));
|
let mut fp = File::open(&path).expect(&format!("Can't open {}", path));
|
||||||
let mut file_content = vec![];
|
let mut file_content = vec![];
|
||||||
|
|
||||||
fp.read_to_end(&mut file_content).expect(&format!("Reading {} failed.", path));
|
fp.read_to_end(&mut file_content).expect(&format!("Reading {} failed.", path));
|
||||||
|
|
Loading…
Reference in New Issue