mirror of https://github.com/rwf2/Rocket.git
Improve FileServer rewrite API.
Finalizes the FileServer rewrite API implementation. Primarily reworks how the built-in rewriters are written (now as structs instead of free functions) and reorganizes the `fs` module. Co-authored-by: Matthew Pomes <matthew.pomes@pm.me>
This commit is contained in:
parent
65e3b87d6b
commit
f50b6043e8
|
@ -178,8 +178,9 @@ impl<'a> Segments<'a, Path> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a `PathBuf` from `self`. The returned `PathBuf` is
|
/// Creates a `PathBuf` from `self`. The returned `PathBuf` is
|
||||||
/// percent-decoded. If a segment is equal to `..`, the previous segment (if
|
/// percent-decoded and guaranteed to be relative. If a segment is equal to
|
||||||
/// any) is skipped.
|
/// `.`, it is skipped. If a segment is equal to `..`, the previous segment
|
||||||
|
/// (if any) is skipped.
|
||||||
///
|
///
|
||||||
/// For security purposes, if a segment meets any of the following
|
/// For security purposes, if a segment meets any of the following
|
||||||
/// conditions, an `Err` is returned indicating the condition met:
|
/// conditions, an `Err` is returned indicating the condition met:
|
||||||
|
@ -193,7 +194,7 @@ impl<'a> Segments<'a, Path> {
|
||||||
/// Additionally, if `allow_dotfiles` is `false`, an `Err` is returned if
|
/// Additionally, if `allow_dotfiles` is `false`, an `Err` is returned if
|
||||||
/// the following condition is met:
|
/// the following condition is met:
|
||||||
///
|
///
|
||||||
/// * Decoded segment starts with any of: `.` (except `..`)
|
/// * Decoded segment starts with any of: `.` (except `..` and `.`)
|
||||||
///
|
///
|
||||||
/// As a result of these conditions, a `PathBuf` derived via `FromSegments`
|
/// As a result of these conditions, a `PathBuf` derived via `FromSegments`
|
||||||
/// is safe to interpolate within, or use as a suffix of, a path without
|
/// is safe to interpolate within, or use as a suffix of, a path without
|
||||||
|
@ -216,10 +217,10 @@ impl<'a> Segments<'a, Path> {
|
||||||
pub fn to_path_buf(&self, allow_dotfiles: bool) -> Result<PathBuf, PathError> {
|
pub fn to_path_buf(&self, allow_dotfiles: bool) -> Result<PathBuf, PathError> {
|
||||||
let mut buf = PathBuf::new();
|
let mut buf = PathBuf::new();
|
||||||
for segment in self.clone() {
|
for segment in self.clone() {
|
||||||
if segment == ".." {
|
if segment == "." {
|
||||||
buf.pop();
|
|
||||||
} else if segment == "." {
|
|
||||||
continue;
|
continue;
|
||||||
|
} else if segment == ".." {
|
||||||
|
buf.pop();
|
||||||
} else if !allow_dotfiles && segment.starts_with('.') {
|
} else if !allow_dotfiles && segment.starts_with('.') {
|
||||||
return Err(PathError::BadStart('.'))
|
return Err(PathError::BadStart('.'))
|
||||||
} else if segment.starts_with('*') {
|
} else if segment.starts_with('*') {
|
||||||
|
|
|
@ -5,8 +5,59 @@ mod named_file;
|
||||||
mod temp_file;
|
mod temp_file;
|
||||||
mod file_name;
|
mod file_name;
|
||||||
|
|
||||||
|
pub mod rewrite;
|
||||||
|
|
||||||
pub use server::*;
|
pub use server::*;
|
||||||
pub use named_file::*;
|
pub use named_file::*;
|
||||||
pub use temp_file::*;
|
pub use temp_file::*;
|
||||||
pub use file_name::*;
|
pub use file_name::*;
|
||||||
pub use server::relative;
|
|
||||||
|
crate::export! {
|
||||||
|
/// Generates a crate-relative version of a path.
|
||||||
|
///
|
||||||
|
/// This macro is primarily intended for use with [`FileServer`] to serve
|
||||||
|
/// files from a path relative to the crate root.
|
||||||
|
///
|
||||||
|
/// The macro accepts one parameter, `$path`, an absolute or (preferably)
|
||||||
|
/// relative path. It returns a path as an `&'static str` prefixed with the
|
||||||
|
/// path to the crate root. Use `Path::new(relative!($path))` to retrieve an
|
||||||
|
/// `&'static Path`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// Serve files from the crate-relative `static/` directory:
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # #[macro_use] extern crate rocket;
|
||||||
|
/// use rocket::fs::{FileServer, relative};
|
||||||
|
///
|
||||||
|
/// #[launch]
|
||||||
|
/// fn rocket() -> _ {
|
||||||
|
/// rocket::build().mount("/", FileServer::new(relative!("static")))
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Path equivalences:
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use std::path::Path;
|
||||||
|
///
|
||||||
|
/// use rocket::fs::relative;
|
||||||
|
///
|
||||||
|
/// let manual = Path::new(env!("CARGO_MANIFEST_DIR")).join("static");
|
||||||
|
/// let automatic_1 = Path::new(relative!("static"));
|
||||||
|
/// let automatic_2 = Path::new(relative!("/static"));
|
||||||
|
/// assert_eq!(manual, automatic_1);
|
||||||
|
/// assert_eq!(automatic_1, automatic_2);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
macro_rules! relative {
|
||||||
|
($path:expr) => {
|
||||||
|
if cfg!(windows) {
|
||||||
|
concat!(env!("CARGO_MANIFEST_DIR"), "\\", $path)
|
||||||
|
} else {
|
||||||
|
concat!(env!("CARGO_MANIFEST_DIR"), "/", $path)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,261 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::Request;
|
||||||
|
use crate::http::{ext::IntoOwned, HeaderMap};
|
||||||
|
use crate::response::Redirect;
|
||||||
|
|
||||||
|
/// A file server [`Rewrite`] rewriter.
|
||||||
|
///
|
||||||
|
/// A [`FileServer`] is a sequence of [`Rewriter`]s which transform the incoming
|
||||||
|
/// request path into a [`Rewrite`] or `None`. The first rewriter is called with
|
||||||
|
/// the request path as a [`Rewrite::File`]. Each `Rewriter` thereafter is
|
||||||
|
/// called in-turn with the previously returned [`Rewrite`], and the value
|
||||||
|
/// returned from the last `Rewriter` is used to respond to the request. If the
|
||||||
|
/// final rewrite is `None` or a nonexistent path or a directory, [`FileServer`]
|
||||||
|
/// responds with [`Status::NotFound`]. Otherwise it responds with the file
|
||||||
|
/// contents, if [`Rewrite::File`] is specified, or a redirect, if
|
||||||
|
/// [`Rewrite::Redirect`] is specified.
|
||||||
|
///
|
||||||
|
/// [`FileServer`]: super::FileServer
|
||||||
|
/// [`Status::NotFound`]: crate::http::Status::NotFound
|
||||||
|
pub trait Rewriter: Send + Sync + 'static {
|
||||||
|
/// Alter the [`Rewrite`] as needed.
|
||||||
|
fn rewrite<'r>(&self, opt: Option<Rewrite<'r>>, req: &'r Request<'_>) -> Option<Rewrite<'r>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Response from a [`FileServer`](super::FileServer)
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum Rewrite<'r> {
|
||||||
|
/// Return the contents of the specified file.
|
||||||
|
File(File<'r>),
|
||||||
|
/// Returns a Redirect.
|
||||||
|
Redirect(Redirect),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A File response from a [`FileServer`](super::FileServer) and a rewriter.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct File<'r> {
|
||||||
|
/// The path to the file that [`FileServer`](super::FileServer) will respond with.
|
||||||
|
pub path: Cow<'r, Path>,
|
||||||
|
/// A list of headers to be added to the generated response.
|
||||||
|
pub headers: HeaderMap<'r>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r> File<'r> {
|
||||||
|
/// A new `File`, with not additional headers.
|
||||||
|
pub fn new(path: impl Into<Cow<'r, Path>>) -> Self {
|
||||||
|
Self { path: path.into(), headers: HeaderMap::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A new `File`, with not additional headers.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the `path` does not exist.
|
||||||
|
pub fn checked<P: AsRef<Path>>(path: P) -> Self {
|
||||||
|
let path = path.as_ref();
|
||||||
|
if !path.exists() {
|
||||||
|
let path = path.display();
|
||||||
|
error!(%path, "FileServer path does not exist.\n\
|
||||||
|
Panicking to prevent inevitable handler error.");
|
||||||
|
panic!("missing file {}: refusing to continue", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::new(path.to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace the path in `self` with the result of applying `f` to the path.
|
||||||
|
pub fn map_path<F, P>(self, f: F) -> Self
|
||||||
|
where F: FnOnce(Cow<'r, Path>) -> P, P: Into<Cow<'r, Path>>,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
path: f(self.path).into(),
|
||||||
|
headers: self.headers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the file is a dotfile. A dotfile is a file whose
|
||||||
|
/// name or any directory in it's path start with a period (`.`) and is
|
||||||
|
/// considered hidden.
|
||||||
|
///
|
||||||
|
/// # Windows Note
|
||||||
|
///
|
||||||
|
/// This does *not* check the file metadata on any platform, so hidden files
|
||||||
|
/// on Windows will not be detected by this method.
|
||||||
|
pub fn is_hidden(&self) -> bool {
|
||||||
|
self.path.iter().any(|s| s.as_encoded_bytes().starts_with(b"."))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the file is not hidden. This is the inverse of
|
||||||
|
/// [`File::is_hidden()`].
|
||||||
|
pub fn is_visible(&self) -> bool {
|
||||||
|
!self.is_hidden()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prefixes all paths with a given path.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use rocket::fs::FileServer;
|
||||||
|
/// use rocket::fs::rewrite::Prefix;
|
||||||
|
///
|
||||||
|
/// FileServer::identity()
|
||||||
|
/// .filter(|f, _| f.is_visible())
|
||||||
|
/// .rewrite(Prefix::checked("static"));
|
||||||
|
/// ```
|
||||||
|
pub struct Prefix(PathBuf);
|
||||||
|
|
||||||
|
impl Prefix {
|
||||||
|
/// Panics if `path` does not exist.
|
||||||
|
pub fn checked<P: AsRef<Path>>(path: P) -> Self {
|
||||||
|
let path = path.as_ref();
|
||||||
|
if !path.is_dir() {
|
||||||
|
let path = path.display();
|
||||||
|
error!(%path, "FileServer path is not a directory.");
|
||||||
|
warn!("Aborting early to prevent inevitable handler error.");
|
||||||
|
panic!("invalid directory: refusing to continue");
|
||||||
|
}
|
||||||
|
|
||||||
|
Self(path.to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `Prefix` from a path.
|
||||||
|
pub fn unchecked<P: AsRef<Path>>(path: P) -> Self {
|
||||||
|
Self(path.as_ref().to_path_buf())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rewriter for Prefix {
|
||||||
|
fn rewrite<'r>(&self, opt: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
|
||||||
|
opt.map(|r| match r {
|
||||||
|
Rewrite::File(f) => Rewrite::File(f.map_path(|p| self.0.join(p))),
|
||||||
|
Rewrite::Redirect(r) => Rewrite::Redirect(r),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rewriter for PathBuf {
|
||||||
|
fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
|
||||||
|
Some(Rewrite::File(File::new(self.clone())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Normalize directories to always include a trailing slash by redirecting
|
||||||
|
/// (with a 302 temporary redirect) requests for directories without a trailing
|
||||||
|
/// slash to the same path with a trailing slash.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use rocket::fs::FileServer;
|
||||||
|
/// use rocket::fs::rewrite::{Prefix, TrailingDirs};
|
||||||
|
///
|
||||||
|
/// FileServer::identity()
|
||||||
|
/// .filter(|f, _| f.is_visible())
|
||||||
|
/// .rewrite(TrailingDirs);
|
||||||
|
/// ```
|
||||||
|
pub struct TrailingDirs;
|
||||||
|
|
||||||
|
impl Rewriter for TrailingDirs {
|
||||||
|
fn rewrite<'r>(&self, opt: Option<Rewrite<'r>>, req: &Request<'_>) -> Option<Rewrite<'r>> {
|
||||||
|
if let Some(Rewrite::File(f)) = &opt {
|
||||||
|
if !req.uri().path().ends_with('/') && f.path.is_dir() {
|
||||||
|
let uri = req.uri().clone().into_owned();
|
||||||
|
let uri = uri.map_path(|p| format!("{p}/")).unwrap();
|
||||||
|
return Some(Rewrite::Redirect(Redirect::temporary(uri)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rewrite a directory to a file inside of that directory.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// Rewrites all directory requests to `directory/index.html`.
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use rocket::fs::FileServer;
|
||||||
|
/// use rocket::fs::rewrite::DirIndex;
|
||||||
|
///
|
||||||
|
/// FileServer::without_index("static")
|
||||||
|
/// .rewrite(DirIndex::if_exists("index.htm"))
|
||||||
|
/// .rewrite(DirIndex::unconditional("index.html"));
|
||||||
|
/// ```
|
||||||
|
pub struct DirIndex {
|
||||||
|
path: PathBuf,
|
||||||
|
check: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DirIndex {
|
||||||
|
/// Appends `path` to every request for a directory.
|
||||||
|
pub fn unconditional(path: impl AsRef<Path>) -> Self {
|
||||||
|
Self { path: path.as_ref().to_path_buf(), check: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only appends `path` to a request for a directory if the file exists.
|
||||||
|
pub fn if_exists(path: impl AsRef<Path>) -> Self {
|
||||||
|
Self { path: path.as_ref().to_path_buf(), check: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rewriter for DirIndex {
|
||||||
|
fn rewrite<'r>(&self, opt: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
|
||||||
|
match opt? {
|
||||||
|
Rewrite::File(f) if f.path.is_dir() => {
|
||||||
|
let candidate = f.path.join(&self.path);
|
||||||
|
if self.check && !candidate.is_file() {
|
||||||
|
return Some(Rewrite::File(f));
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Rewrite::File(f.map_path(|_| candidate)))
|
||||||
|
}
|
||||||
|
r => Some(r),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r> From<File<'r>> for Rewrite<'r> {
|
||||||
|
fn from(value: File<'r>) -> Self {
|
||||||
|
Self::File(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r> From<Redirect> for Rewrite<'r> {
|
||||||
|
fn from(value: Redirect) -> Self {
|
||||||
|
Self::Redirect(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: Send + Sync + 'static> Rewriter for F
|
||||||
|
where F: for<'r> Fn(Option<Rewrite<'r>>, &Request<'_>) -> Option<Rewrite<'r>>
|
||||||
|
{
|
||||||
|
fn rewrite<'r>(&self, f: Option<Rewrite<'r>>, r: &Request<'_>) -> Option<Rewrite<'r>> {
|
||||||
|
self(f, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rewriter for Rewrite<'static> {
|
||||||
|
fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
|
||||||
|
Some(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rewriter for File<'static> {
|
||||||
|
fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
|
||||||
|
Some(Rewrite::File(self.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rewriter for Redirect {
|
||||||
|
fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
|
||||||
|
Some(Rewrite::Redirect(self.clone()))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,46 +1,21 @@
|
||||||
use core::fmt;
|
use std::fmt;
|
||||||
use std::borrow::Cow;
|
use std::path::{Path, PathBuf};
|
||||||
use std::ffi::OsStr;
|
|
||||||
use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use crate::fs::NamedFile;
|
use crate::{response, Data, Request, Response};
|
||||||
use crate::{Data, Request, outcome::IntoOutcome};
|
use crate::outcome::IntoOutcome;
|
||||||
use crate::http::{
|
use crate::http::{uri::Segments, HeaderMap, Method, ContentType, Status};
|
||||||
Method,
|
|
||||||
HeaderMap,
|
|
||||||
Header,
|
|
||||||
uri::Segments,
|
|
||||||
Status,
|
|
||||||
ext::IntoOwned,
|
|
||||||
};
|
|
||||||
use crate::route::{Route, Handler, Outcome};
|
use crate::route::{Route, Handler, Outcome};
|
||||||
use crate::response::{Redirect, Responder};
|
use crate::response::Responder;
|
||||||
|
use crate::util::Formatter;
|
||||||
|
use crate::fs::rewrite::*;
|
||||||
|
|
||||||
/// Custom handler for serving static files.
|
/// Custom handler for serving static files.
|
||||||
///
|
///
|
||||||
/// This handler makes is simple to serve static files from a directory on the
|
/// This handler makes is simple to serve static files from a directory on the
|
||||||
/// local file system. To use it, construct a `FileServer` using
|
/// local file system. To use it, construct a `FileServer` using
|
||||||
/// [`FileServer::from()`], then simply `mount` the handler. When mounted, the
|
/// [`FileServer::new()`], then `mount` the handler.
|
||||||
/// handler serves files from the specified directory. If the file is not found,
|
|
||||||
/// the handler _forwards_ the request. By default, `FileServer` has a rank of
|
|
||||||
/// `10`. Use [`FileServer::new()`] to create a route with a custom rank.
|
|
||||||
///
|
|
||||||
/// # Customization
|
|
||||||
///
|
|
||||||
/// How `FileServer` responds to specific requests can be customized, through
|
|
||||||
/// the use of [`Rewriter`]s. See [`Rewriter`] for more detailed documentation
|
|
||||||
/// on how to take full advantage of the customization of `FileServer`.
|
|
||||||
///
|
|
||||||
/// [`FileServer::from()`] and [`FileServer::new()`] automatically add some common
|
|
||||||
/// rewrites. They filter out dotfiles, redirect folder accesses to include a trailing
|
|
||||||
/// slash, and use `index.html` to respond to requests for a directory. If you want
|
|
||||||
/// to customize or replace these default rewrites, see [`FileServer::empty()`].
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// Serve files from the `/static` directory on the local file system at the
|
|
||||||
/// `/public` path, with the default rewrites.
|
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # #[macro_use] extern crate rocket;
|
/// # #[macro_use] extern crate rocket;
|
||||||
|
@ -48,7 +23,38 @@ use crate::response::{Redirect, Responder};
|
||||||
///
|
///
|
||||||
/// #[launch]
|
/// #[launch]
|
||||||
/// fn rocket() -> _ {
|
/// fn rocket() -> _ {
|
||||||
/// rocket::build().mount("/public", FileServer::from("/static"))
|
/// rocket::build()
|
||||||
|
/// .mount("/", FileServer::new("/www/static"))
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// When mounted, the handler serves files from the specified path. If a
|
||||||
|
/// requested file does not exist, the handler _forwards_ the request with a
|
||||||
|
/// `404` status.
|
||||||
|
///
|
||||||
|
/// By default, the route has a rank of `10` which can be changed with
|
||||||
|
/// [`FileServer::rank()`].
|
||||||
|
///
|
||||||
|
/// # Customization
|
||||||
|
///
|
||||||
|
/// `FileServer` works through a pipeline of _rewrites_ in which a requested
|
||||||
|
/// path is transformed into a `PathBuf` via [`Segments::to_path_buf()`] and
|
||||||
|
/// piped through a series of [`Rewriter`]s to obtain a final [`Rewrite`] which
|
||||||
|
/// is then used to generate a final response. See [`Rewriter`] for complete
|
||||||
|
/// details on implementing your own `Rewriter`s.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// Serve files from the `/static` directory on the local file system at the
|
||||||
|
/// `/public` path:
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # #[macro_use] extern crate rocket;
|
||||||
|
/// use rocket::fs::FileServer;
|
||||||
|
///
|
||||||
|
/// #[launch]
|
||||||
|
/// fn rocket() -> _ {
|
||||||
|
/// rocket::build().mount("/public", FileServer::new("/static"))
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
|
@ -70,589 +76,331 @@ use crate::response::{Redirect, Responder};
|
||||||
///
|
///
|
||||||
/// #[launch]
|
/// #[launch]
|
||||||
/// fn rocket() -> _ {
|
/// fn rocket() -> _ {
|
||||||
/// rocket::build().mount("/", FileServer::from(relative!("static")))
|
/// rocket::build().mount("/", FileServer::new(relative!("static")))
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`relative!`]: crate::fs::relative!
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FileServer {
|
pub struct FileServer {
|
||||||
rewrites: Vec<Arc<dyn Rewriter>>,
|
rewrites: Vec<Arc<dyn Rewriter>>,
|
||||||
rank: isize,
|
rank: isize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for FileServer {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_struct("FileServer")
|
|
||||||
// .field("root", &self.root)
|
|
||||||
.field("rewrites", &DebugListRewrite(&self.rewrites))
|
|
||||||
.field("rank", &self.rank)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DebugListRewrite<'a>(&'a Vec<Arc<dyn Rewriter>>);
|
|
||||||
|
|
||||||
impl fmt::Debug for DebugListRewrite<'_> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "<{} rewrites>", self.0.len())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait used to implement [`FileServer`] customization.
|
|
||||||
///
|
|
||||||
/// Conceptually, a [`FileServer`] is a sequence of `Rewriter`s, which transform
|
|
||||||
/// a path from a request to a final response. [`FileServer`] add a set of default
|
|
||||||
/// `Rewriter`s, which filter out dotfiles, apply a root path, normalize directories,
|
|
||||||
/// and use `index.html`.
|
|
||||||
///
|
|
||||||
/// After running the chain of `Rewriter`s,
|
|
||||||
/// [`FileServer`] uses the final [`Option<FileResponse>`](FileResponse)
|
|
||||||
/// to respond to the request. If the response is `None`, a path that doesn't
|
|
||||||
/// exist or a directory path, [`FileServer`] will respond with a
|
|
||||||
/// [`Status::NotFound`](crate::http::Status::NotFound). Otherwise the [`FileServer`]
|
|
||||||
/// will respond with a redirect or the contents of the file specified.
|
|
||||||
///
|
|
||||||
/// [`FileServer`] provides several helper methods to add `Rewriter`s:
|
|
||||||
/// - [`FileServer::and_rewrite()`]
|
|
||||||
/// - [`FileServer::filter_file()`]
|
|
||||||
/// - [`FileServer::map_file()`]
|
|
||||||
pub trait Rewriter: Send + Sync + 'static {
|
|
||||||
/// Alter the [`FileResponse`] as needed.
|
|
||||||
fn rewrite<'p, 'h>(&self, path: Option<FileResponse<'p, 'h>>, req: &Request<'_>)
|
|
||||||
-> Option<FileResponse<'p, 'h>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A Response from a [`FileServer`]
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum FileResponse<'p, 'h> {
|
|
||||||
/// Return the contents of the specified file.
|
|
||||||
File(File<'p, 'h>),
|
|
||||||
/// Returns a Redirect to the specified path. This needs to be an absolute
|
|
||||||
/// URI, so you should start with [`File.full_uri`](File) when constructing
|
|
||||||
/// a redirect.
|
|
||||||
Redirect(Redirect),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'p, 'h> From<File<'p, 'h>> for FileResponse<'p, 'h> {
|
|
||||||
fn from(value: File<'p, 'h>) -> Self {
|
|
||||||
Self::File(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'p, 'h> From<File<'p, 'h>> for Option<FileResponse<'p, 'h>> {
|
|
||||||
fn from(value: File<'p, 'h>) -> Self {
|
|
||||||
Some(FileResponse::File(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'p, 'h> From<Redirect> for FileResponse<'p, 'h> {
|
|
||||||
fn from(value: Redirect) -> Self {
|
|
||||||
Self::Redirect(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'p, 'h> From<Redirect> for Option<FileResponse<'p, 'h>> {
|
|
||||||
fn from(value: Redirect) -> Self {
|
|
||||||
Some(FileResponse::Redirect(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A File response from a [`FileServer`]
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct File<'p, 'h> {
|
|
||||||
/// The path to the file that [`FileServer`] will respond with.
|
|
||||||
pub path: Cow<'p, Path>,
|
|
||||||
/// A list of headers to be added to the generated response.
|
|
||||||
pub headers: HeaderMap<'h>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'p, 'h> File<'p, 'h> {
|
|
||||||
/// Add a header to this `File`.
|
|
||||||
pub fn with_header<'n: 'h, H: Into<Header<'n>>>(mut self, header: H) -> Self {
|
|
||||||
self.headers.add(header);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replace the path of this `File`.
|
|
||||||
pub fn with_path(self, path: impl Into<Cow<'p, Path>>) -> Self {
|
|
||||||
Self {
|
|
||||||
path: path.into(),
|
|
||||||
headers: self.headers,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replace the path of this `File`.
|
|
||||||
pub fn map_path<R: Into<Cow<'p, Path>>>(self, f: impl FnOnce(Cow<'p, Path>) -> R) -> Self {
|
|
||||||
Self {
|
|
||||||
path: f(self.path).into(),
|
|
||||||
headers: self.headers,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// /// Convert this `File` into a Redirect, transforming the URI.
|
|
||||||
// pub fn into_redirect(self, f: impl FnOnce(Origin<'static>) -> Origin<'static>)
|
|
||||||
// -> FileResponse<'p, 'h>
|
|
||||||
// {
|
|
||||||
// FileResponse::Redirect(Redirect::permanent(f(self.full_uri.clone().into_owned())))
|
|
||||||
// }
|
|
||||||
|
|
||||||
async fn respond_to<'r>(self, req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r>
|
|
||||||
where 'h: 'r
|
|
||||||
{
|
|
||||||
/// Normalize paths to enable `file_root` to work properly
|
|
||||||
fn strip_trailing_slash(p: &Path) -> &Path {
|
|
||||||
let bytes = p.as_os_str().as_encoded_bytes();
|
|
||||||
let bytes = bytes.strip_suffix(MAIN_SEPARATOR_STR.as_bytes()).unwrap_or(bytes);
|
|
||||||
// SAFETY: Since we stripped a valid UTF-8 sequence (or left it unchanged),
|
|
||||||
// this is still a valid OsStr.
|
|
||||||
Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(bytes) })
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = strip_trailing_slash(self.path.as_ref());
|
|
||||||
// Fun fact, on Linux attempting to open a directory works, it just errors
|
|
||||||
// when you attempt to read it.
|
|
||||||
if path.is_file() {
|
|
||||||
NamedFile::open(path)
|
|
||||||
.await
|
|
||||||
.respond_to(req)
|
|
||||||
.map(|mut r| {
|
|
||||||
for header in self.headers {
|
|
||||||
r.adjoin_raw_header(header.name.as_str().to_owned(), header.value);
|
|
||||||
}
|
|
||||||
r
|
|
||||||
}).or_forward((data, Status::NotFound))
|
|
||||||
} else {
|
|
||||||
Outcome::forward(data, Status::NotFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<F: Send + Sync + 'static> Rewriter for F
|
|
||||||
where F: for<'r, 'h> Fn(Option<FileResponse<'r, 'h>>, &Request<'_>)
|
|
||||||
-> Option<FileResponse<'r, 'h>>
|
|
||||||
{
|
|
||||||
fn rewrite<'p, 'h>(&self, path: Option<FileResponse<'p, 'h>>, req: &Request<'_>)
|
|
||||||
-> Option<FileResponse<'p, 'h>>
|
|
||||||
{
|
|
||||||
self(path, req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper to implement [`FileServer::filter_file()`]
|
|
||||||
struct FilterFile<F>(F);
|
|
||||||
impl<F> Rewriter for FilterFile<F>
|
|
||||||
where F: Fn(&File<'_, '_>, &Request<'_>) -> bool + Send + Sync + 'static
|
|
||||||
{
|
|
||||||
fn rewrite<'p, 'h>(&self, path: Option<FileResponse<'p, 'h>>, req: &Request<'_>)
|
|
||||||
-> Option<FileResponse<'p, 'h>>
|
|
||||||
{
|
|
||||||
match path {
|
|
||||||
Some(FileResponse::File(file)) if !self.0(&file, req) => None,
|
|
||||||
path => path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper to implement [`FileServer::map_file()`]
|
|
||||||
struct MapFile<F>(F);
|
|
||||||
impl<F> Rewriter for MapFile<F>
|
|
||||||
where F: for<'p, 'h> Fn(File<'p, 'h>, &Request<'_>)
|
|
||||||
-> FileResponse<'p, 'h> + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
fn rewrite<'p, 'h>(&self, path: Option<FileResponse<'p, 'h>>, req: &Request<'_>)
|
|
||||||
-> Option<FileResponse<'p, 'h>>
|
|
||||||
{
|
|
||||||
match path {
|
|
||||||
Some(FileResponse::File(file)) => Some(self.0(file, req)),
|
|
||||||
path => path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper trait to simplify standard rewrites
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub trait FileMap:
|
|
||||||
for<'p, 'h> Fn(File<'p, 'h>, &Request<'_>) -> FileResponse<'p, 'h> + Send + Sync + 'static
|
|
||||||
{}
|
|
||||||
impl<F> FileMap for F
|
|
||||||
where F: for<'p, 'h> Fn(File<'p, 'h>, &Request<'_>)
|
|
||||||
-> FileResponse<'p, 'h> + Send + Sync + 'static
|
|
||||||
{}
|
|
||||||
/// Helper trait to simplify standard rewrites
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub trait FileFilter: Fn(&File<'_, '_>, &Request<'_>) -> bool + Send + Sync + 'static {}
|
|
||||||
impl<F> FileFilter for F
|
|
||||||
where F: Fn(&File<'_, '_>, &Request<'_>) -> bool + Send + Sync + 'static
|
|
||||||
{}
|
|
||||||
|
|
||||||
/// Prepends the provided path, to serve files from a directory.
|
|
||||||
///
|
|
||||||
/// You can use [`relative!`] to make a path relative to the crate root, rather
|
|
||||||
/// than the runtime directory.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// # use rocket::fs::{FileServer, dir_root, relative};
|
|
||||||
/// # fn make_server() -> FileServer {
|
|
||||||
/// FileServer::empty()
|
|
||||||
/// .map_file(dir_root(relative!("static")))
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// Panics if `path` does not exist. See [`file_root_permissive`] for a
|
|
||||||
/// non-panicing variant.
|
|
||||||
pub fn dir_root(path: impl AsRef<Path>) -> impl FileMap {
|
|
||||||
let path = path.as_ref();
|
|
||||||
if !path.is_dir() {
|
|
||||||
let path = path.display();
|
|
||||||
error!(%path, "FileServer path is not a directory.");
|
|
||||||
warn!("Aborting early to prevent inevitable handler error.");
|
|
||||||
panic!("invalid directory: refusing to continue");
|
|
||||||
}
|
|
||||||
let path = path.to_path_buf();
|
|
||||||
move |f, _r| {
|
|
||||||
FileResponse::File(f.map_path(|p| path.join(p)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prepends the provided path, to serve a single static file.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// # use rocket::fs::{FileServer, file_root};
|
|
||||||
/// # fn make_server() -> FileServer {
|
|
||||||
/// FileServer::empty()
|
|
||||||
/// .map_file(file_root("static/index.html"))
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// Panics if `path` does not exist. See [`file_root_permissive`] for a
|
|
||||||
/// non-panicing variant.
|
|
||||||
pub fn file_root(path: impl AsRef<Path>) -> impl FileMap {
|
|
||||||
let path = path.as_ref();
|
|
||||||
if !path.exists() {
|
|
||||||
let path = path.display();
|
|
||||||
error!(%path, "FileServer path does not exist.");
|
|
||||||
warn!("Aborting early to prevent inevitable handler error.");
|
|
||||||
panic!("invalid file: refusing to continue");
|
|
||||||
}
|
|
||||||
let path = path.to_path_buf();
|
|
||||||
move |f, _r| {
|
|
||||||
FileResponse::File(f.map_path(|p| path.join(p)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prepends the provided path, without checking to ensure the path exists during
|
|
||||||
/// startup.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// # use rocket::fs::{FileServer, file_root_permissive};
|
|
||||||
/// # fn make_server() -> FileServer {
|
|
||||||
/// FileServer::empty()
|
|
||||||
/// .map_file(file_root_permissive("/tmp/rocket"))
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn file_root_permissive(path: impl AsRef<Path>) -> impl FileMap {
|
|
||||||
let path = path.as_ref().to_path_buf();
|
|
||||||
move |f, _r| {
|
|
||||||
FileResponse::File(f.map_path(|p| path.join(p)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Filters out any path that contains a file or directory name starting with a
|
|
||||||
/// dot. If used after `dir_root`, this will also check the root path for dots, and
|
|
||||||
/// filter them.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// # use rocket::fs::{FileServer, filter_dotfiles, dir_root};
|
|
||||||
/// # fn make_server() -> FileServer {
|
|
||||||
/// FileServer::empty()
|
|
||||||
/// .filter_file(filter_dotfiles)
|
|
||||||
/// .map_file(dir_root("static"))
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn filter_dotfiles(file: &File<'_, '_>, _req: &Request<'_>) -> bool {
|
|
||||||
!file.path.iter().any(|s| s.as_encoded_bytes().starts_with(b"."))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Normalize directory accesses to always include a trailing slash.
|
|
||||||
///
|
|
||||||
/// Should normally be used after `dir_root` (or another rewrite that adds
|
|
||||||
/// a root), since it needs the full path to check whether a path points to
|
|
||||||
/// a directory.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// Appends a slash to any request for a directory without a trailing slash
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// # use rocket::fs::{FileServer, normalize_dirs, dir_root};
|
|
||||||
/// # fn make_server() -> FileServer {
|
|
||||||
/// FileServer::empty()
|
|
||||||
/// .map_file(dir_root("static"))
|
|
||||||
/// .map_file(normalize_dirs)
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn normalize_dirs<'p, 'h>(file: File<'p, 'h>, req: &Request<'_>) -> FileResponse<'p, 'h> {
|
|
||||||
if !req.uri().path().raw().ends_with('/') && file.path.is_dir() {
|
|
||||||
FileResponse::Redirect(Redirect::permanent(
|
|
||||||
// Known good path + '/' is a good path
|
|
||||||
req.uri().clone().into_owned().map_path(|p| format!("{p}/")).unwrap()
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
FileResponse::File(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Appends a file name to all directory accesses.
|
|
||||||
///
|
|
||||||
/// Must be used after `dir_root`, since it needs the full path to check whether it is
|
|
||||||
/// a directory.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// Appends `index.html` to any directory access.
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// # use rocket::fs::{FileServer, index, dir_root};
|
|
||||||
/// # fn make_server() -> FileServer {
|
|
||||||
/// FileServer::empty()
|
|
||||||
/// .map_file(dir_root("static"))
|
|
||||||
/// .map_file(index("index.html"))
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn index(index: &'static str) -> impl FileMap {
|
|
||||||
move |f, _r| if f.path.is_dir() {
|
|
||||||
FileResponse::File(f.map_path(|p| p.join(index)))
|
|
||||||
} else {
|
|
||||||
FileResponse::File(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileServer {
|
impl FileServer {
|
||||||
/// The default rank use by `FileServer` routes.
|
/// The default rank use by `FileServer` routes.
|
||||||
const DEFAULT_RANK: isize = 10;
|
const DEFAULT_RANK: isize = 10;
|
||||||
|
|
||||||
/// Constructs a new `FileServer`, with default rank, and no
|
/// Constructs a new `FileServer` that serves files from the file system
|
||||||
/// rewrites.
|
/// `path` with the following rewrites:
|
||||||
///
|
///
|
||||||
/// See [`FileServer::empty_ranked()`].
|
/// - `|f, _| f.is_visible()`: Serve only visible files (hide dotfiles).
|
||||||
pub fn empty() -> Self {
|
/// - [`Prefix::checked(path)`]: Prefix requests with `path`.
|
||||||
Self::empty_ranked(Self::DEFAULT_RANK)
|
/// - [`TrailingDirs`]: Ensure directory have a trailing slash.
|
||||||
}
|
/// - [`DirIndex::unconditional("index.html")`]: Serve `$dir/index.html` for
|
||||||
|
/// requests to directory `$dir`.
|
||||||
/// Constructs a new `FileServer`, with specified rank, and no
|
///
|
||||||
/// rewrites.
|
/// If you don't want to serve index files or want a different index file,
|
||||||
|
/// use [`Self::without_index`]. To customize the entire request to file
|
||||||
|
/// path rewrite pipeline, use [`Self::identity`].
|
||||||
|
///
|
||||||
|
/// [`Prefix::checked(path)`]: crate::fs::rewrite::Prefix::checked
|
||||||
|
/// [`TrailingDirs`]: crate::fs::rewrite::TrailingDirs
|
||||||
|
/// [`DirIndex::unconditional("index.html")`]: DirIndex::unconditional()
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// Replicate the output of [`FileServer::new()`].
|
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # use rocket::fs::{FileServer, filter_dotfiles, dir_root, normalize_dirs};
|
/// # #[macro_use] extern crate rocket;
|
||||||
/// # fn launch() -> FileServer {
|
/// use rocket::fs::FileServer;
|
||||||
/// FileServer::empty_ranked(10)
|
///
|
||||||
/// .filter_file(filter_dotfiles)
|
/// #[launch]
|
||||||
/// .map_file(dir_root("static"))
|
/// fn rocket() -> _ {
|
||||||
/// .map_file(normalize_dirs)
|
/// rocket::build()
|
||||||
/// # }
|
/// .mount("/", FileServer::new("/www/static"))
|
||||||
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn empty_ranked(rank: isize) -> Self {
|
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
||||||
|
Self::identity()
|
||||||
|
.filter(|f, _| f.is_visible())
|
||||||
|
.rewrite(Prefix::checked(path))
|
||||||
|
.rewrite(TrailingDirs)
|
||||||
|
.rewrite(DirIndex::unconditional("index.html"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exactly like [`FileServer::new()`] except it _does not_ serve directory
|
||||||
|
/// index files via [`DirIndex`]. It rewrites with the following:
|
||||||
|
///
|
||||||
|
/// - `|f, _| f.is_visible()`: Serve only visible files (hide dotfiles).
|
||||||
|
/// - [`Prefix::checked(path)`]: Prefix requests with `path`.
|
||||||
|
/// - [`TrailingDirs`]: Ensure directory have a trailing slash.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// Constructs a default file server to serve files from `./static` using
|
||||||
|
/// `index.txt` as the index file if `index.html` doesn't exist.
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # #[macro_use] extern crate rocket;
|
||||||
|
/// use rocket::fs::{FileServer, rewrite::DirIndex};
|
||||||
|
///
|
||||||
|
/// #[launch]
|
||||||
|
/// fn rocket() -> _ {
|
||||||
|
/// let server = FileServer::new("static")
|
||||||
|
/// .rewrite(DirIndex::if_exists("index.html"))
|
||||||
|
/// .rewrite(DirIndex::unconditional("index.txt"));
|
||||||
|
///
|
||||||
|
/// rocket::build()
|
||||||
|
/// .mount("/", server)
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`Prefix::checked(path)`]: crate::fs::rewrite::Prefix::checked
|
||||||
|
/// [`TrailingDirs`]: crate::fs::rewrite::TrailingDirs
|
||||||
|
pub fn without_index<P: AsRef<Path>>(path: P) -> Self {
|
||||||
|
Self::identity()
|
||||||
|
.filter(|f, _| f.is_visible())
|
||||||
|
.rewrite(Prefix::checked(path))
|
||||||
|
.rewrite(TrailingDirs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a new `FileServer` with no rewrites.
|
||||||
|
///
|
||||||
|
/// Without any rewrites, a `FileServer` will try to serve the requested
|
||||||
|
/// file from the current working directory. In other words, it represents
|
||||||
|
/// the identity rewrite. For example, a request `GET /foo/bar` will be
|
||||||
|
/// passed through unmodified and thus `./foo/bar` will be served. This is
|
||||||
|
/// very unlikely to be what you want.
|
||||||
|
///
|
||||||
|
/// Prefer to use [`FileServer::new()`] or [`FileServer::without_index()`]
|
||||||
|
/// whenever possible and otherwise use one or more of the rewrites in
|
||||||
|
/// [`rocket::fs::rewrite`] or your own custom rewrites.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # #[macro_use] extern crate rocket;
|
||||||
|
/// use rocket::fs::{FileServer, rewrite};
|
||||||
|
///
|
||||||
|
/// #[launch]
|
||||||
|
/// fn rocket() -> _ {
|
||||||
|
/// // A file server that serves exactly one file: /www/foo.html. The
|
||||||
|
/// // file is served irrespective of what's requested.
|
||||||
|
/// let server = FileServer::identity()
|
||||||
|
/// .rewrite(rewrite::File::checked("/www/foo.html"));
|
||||||
|
///
|
||||||
|
/// rocket::build()
|
||||||
|
/// .mount("/", server)
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn identity() -> Self {
|
||||||
Self {
|
Self {
|
||||||
rewrites: vec![],
|
rewrites: vec![],
|
||||||
rank,
|
rank: Self::DEFAULT_RANK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a new `FileServer`, with the defualt rank of 10.
|
/// Sets the rank of the route emitted by the `FileServer` to `rank`.
|
||||||
///
|
|
||||||
/// See [`FileServer::new`].
|
|
||||||
pub fn from<P: AsRef<Path>>(path: P) -> Self {
|
|
||||||
Self::new(path, Self::DEFAULT_RANK)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructs a new `FileServer` that serves files from the file system
|
|
||||||
/// `path`, with the specified rank.
|
|
||||||
///
|
|
||||||
/// Adds a set of default rewrites:
|
|
||||||
/// - [`filter_dotfiles`]: Hides all dotfiles.
|
|
||||||
/// - [`dir_root(path)`](dir_root): Applies the root path.
|
|
||||||
/// - [`normalize_dirs`]: Normalizes directories to have a trailing slash.
|
|
||||||
/// - [`index("index.html")`](index): Appends `index.html` to directory requests.
|
|
||||||
pub fn new<P: AsRef<Path>>(path: P, rank: isize) -> Self {
|
|
||||||
Self::empty_ranked(rank)
|
|
||||||
.filter_file(filter_dotfiles)
|
|
||||||
.map_file(dir_root(path))
|
|
||||||
.map_file(normalize_dirs)
|
|
||||||
.map_file(index("index.html"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generic rewrite to transform one FileResponse to another.
|
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// Redirects all requests that have been filtered to the root of the `FileServer`.
|
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # use rocket::{fs::{FileServer, FileResponse}, response::Redirect,
|
/// # use rocket::fs::FileServer;
|
||||||
/// # uri, Build, Rocket, Request};
|
/// # fn make_server() -> FileServer {
|
||||||
/// fn redir_missing<'p, 'h>(p: Option<FileResponse<'p, 'h>>, _req: &Request<'_>)
|
/// FileServer::identity()
|
||||||
/// -> Option<FileResponse<'p, 'h>>
|
/// .rank(5)
|
||||||
/// {
|
/// # }
|
||||||
/// match p {
|
pub fn rank(mut self, rank: isize) -> Self {
|
||||||
/// None => Redirect::temporary(uri!("/")).into(),
|
self.rank = rank;
|
||||||
/// p => p,
|
self
|
||||||
/// }
|
}
|
||||||
|
|
||||||
|
/// Add `rewriter` to the rewrite pipeline.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// Redirect filtered requests (`None`) to `/`.
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # #[macro_use] extern crate rocket;
|
||||||
|
/// use rocket::fs::{FileServer, rewrite::Rewrite};
|
||||||
|
/// use rocket::{request::Request, response::Redirect};
|
||||||
|
///
|
||||||
|
/// fn redir_missing<'r>(p: Option<Rewrite<'r>>, _req: &Request<'_>) -> Option<Rewrite<'r>> {
|
||||||
|
/// Some(p.unwrap_or_else(|| Redirect::temporary(uri!("/")).into()))
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// # fn launch() -> Rocket<Build> {
|
/// #[launch]
|
||||||
/// rocket::build()
|
/// fn rocket() -> _ {
|
||||||
/// .mount("/", FileServer::from("static").and_rewrite(redir_missing))
|
/// rocket::build()
|
||||||
/// # }
|
/// .mount("/", FileServer::new("static").rewrite(redir_missing))
|
||||||
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// Note that `redir_missing` is not a closure in this example. Making it a closure
|
/// Note that `redir_missing` is not a closure in this example. Making it a closure
|
||||||
/// causes compilation to fail with a lifetime error. It really shouldn't but it does.
|
/// causes compilation to fail with a lifetime error. It really shouldn't but it does.
|
||||||
pub fn and_rewrite(mut self, f: impl Rewriter) -> Self {
|
pub fn rewrite<R: Rewriter>(mut self, rewriter: R) -> Self {
|
||||||
self.rewrites.push(Arc::new(f));
|
self.rewrites.push(Arc::new(rewriter));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filter what files this `FileServer` will respond with
|
/// Adds a rewriter to the pipeline that returns `Some` only when the
|
||||||
|
/// function `f` returns `true`, filtering out all other files.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// Filter out all paths with a filename of `hidden`.
|
/// Allow all files that don't have a file name or have a file name other
|
||||||
|
/// than "hidden".
|
||||||
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # use rocket::{fs::FileServer, response::Redirect, uri, Rocket, Build};
|
/// # #[macro_use] extern crate rocket;
|
||||||
/// # fn launch() -> Rocket<Build> {
|
/// use rocket::fs::FileServer;
|
||||||
/// rocket::build()
|
///
|
||||||
/// .mount(
|
/// #[launch]
|
||||||
/// "/",
|
/// fn rocket() -> _ {
|
||||||
/// FileServer::from("static")
|
/// let server = FileServer::new("static")
|
||||||
/// .filter_file(|f, _r| f.path.file_name() != Some("hidden".as_ref()))
|
/// .filter(|f, _| f.path.file_name() != Some("hidden".as_ref()));
|
||||||
/// )
|
///
|
||||||
/// # }
|
/// rocket::build()
|
||||||
|
/// .mount("/", server)
|
||||||
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn filter_file<F>(self, f: F) -> Self
|
pub fn filter<F: Send + Sync + 'static>(self, f: F) -> Self
|
||||||
where F: Fn(&File<'_, '_>, &Request<'_>) -> bool + Send + Sync + 'static
|
where F: Fn(&File<'_>, &Request<'_>) -> bool
|
||||||
{
|
{
|
||||||
self.and_rewrite(FilterFile(f))
|
struct Filter<F>(F);
|
||||||
|
|
||||||
|
impl<F> Rewriter for Filter<F>
|
||||||
|
where F: Fn(&File<'_>, &Request<'_>) -> bool + Send + Sync + 'static
|
||||||
|
{
|
||||||
|
fn rewrite<'r>(&self, f: Option<Rewrite<'r>>, r: &Request<'_>) -> Option<Rewrite<'r>> {
|
||||||
|
f.and_then(|f| match f {
|
||||||
|
Rewrite::File(f) if self.0(&f, r) => Some(Rewrite::File(f)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rewrite(Filter(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transform files
|
/// Adds a rewriter to the pipeline that maps the current `File` to another
|
||||||
|
/// `Rewrite` using `f`. If the current `Rewrite` is a `Redirect`, it is
|
||||||
|
/// passed through without calling `f`.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// Append `hidden` to the path of every file returned.
|
/// Append `index.txt` to every path.
|
||||||
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # use rocket::{fs::FileServer, Build, Rocket};
|
/// # #[macro_use] extern crate rocket;
|
||||||
/// # fn launch() -> Rocket<Build> {
|
/// use rocket::fs::FileServer;
|
||||||
/// rocket::build()
|
///
|
||||||
/// .mount(
|
/// #[launch]
|
||||||
/// "/",
|
/// fn rocket() -> _ {
|
||||||
/// FileServer::from("static")
|
/// let server = FileServer::new("static")
|
||||||
/// .map_file(|f, _r| f.map_path(|p| p.join("hidden")).into())
|
/// .map(|f, _| f.map_path(|p| p.join("index.txt")).into());
|
||||||
/// )
|
///
|
||||||
/// # }
|
/// rocket::build()
|
||||||
|
/// .mount("/", server)
|
||||||
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn map_file<F>(self, f: F) -> Self
|
pub fn map<F: Send + Sync + 'static>(self, f: F) -> Self
|
||||||
where F: for<'r, 'h> Fn(File<'r, 'h>, &Request<'_>)
|
where F: for<'r> Fn(File<'r>, &Request<'_>) -> Rewrite<'r>
|
||||||
-> FileResponse<'r, 'h> + Send + Sync + 'static
|
|
||||||
{
|
{
|
||||||
self.and_rewrite(MapFile(f))
|
struct Map<F>(F);
|
||||||
|
|
||||||
|
impl<F> Rewriter for Map<F>
|
||||||
|
where F: for<'r> Fn(File<'r>, &Request<'_>) -> Rewrite<'r> + Send + Sync + 'static
|
||||||
|
{
|
||||||
|
fn rewrite<'r>(&self, f: Option<Rewrite<'r>>, r: &Request<'_>) -> Option<Rewrite<'r>> {
|
||||||
|
f.map(|f| match f {
|
||||||
|
Rewrite::File(f) => self.0(f, r),
|
||||||
|
Rewrite::Redirect(r) => Rewrite::Redirect(r),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rewrite(Map(f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<FileServer> for Vec<Route> {
|
impl From<FileServer> for Vec<Route> {
|
||||||
fn from(server: FileServer) -> Self {
|
fn from(server: FileServer) -> Self {
|
||||||
// let source = figment::Source::File(server.root.clone());
|
|
||||||
let mut route = Route::ranked(server.rank, Method::Get, "/<path..>", server);
|
let mut route = Route::ranked(server.rank, Method::Get, "/<path..>", server);
|
||||||
// I'd like to provide a more descriptive name, but we can't get more
|
|
||||||
// information out of `dyn Rewriter`
|
|
||||||
route.name = Some("FileServer".into());
|
route.name = Some("FileServer".into());
|
||||||
vec![route]
|
vec![route]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[crate::async_trait]
|
#[crate::async_trait]
|
||||||
impl Handler for FileServer {
|
impl Handler for FileServer {
|
||||||
async fn handle<'r>(&self, req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r> {
|
async fn handle<'r>(&self, req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r> {
|
||||||
use crate::http::uri::fmt::Path as UriPath;
|
use crate::http::uri::fmt::Path as UriPath;
|
||||||
let path: Option<PathBuf> = req.segments::<Segments<'_, UriPath>>(0..).ok()
|
let path: Option<PathBuf> = req.segments::<Segments<'_, UriPath>>(0..).ok()
|
||||||
.and_then(|segments| segments.to_path_buf(true).ok());
|
.and_then(|segments| segments.to_path_buf(true).ok());
|
||||||
let mut response = path.as_ref().map(|p| FileResponse::File(File {
|
|
||||||
path: Cow::Borrowed(p),
|
|
||||||
headers: HeaderMap::new(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
let mut response = path.map(|p| Rewrite::File(File::new(p)));
|
||||||
for rewrite in &self.rewrites {
|
for rewrite in &self.rewrites {
|
||||||
response = rewrite.rewrite(response, req);
|
response = rewrite.rewrite(response, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
match response {
|
let (outcome, status) = match response {
|
||||||
Some(FileResponse::File(file)) => file.respond_to(req, data).await,
|
Some(Rewrite::File(f)) => (f.open().await.respond_to(req), Status::NotFound),
|
||||||
Some(FileResponse::Redirect(r)) => {
|
Some(Rewrite::Redirect(r)) => (r.respond_to(req), Status::InternalServerError),
|
||||||
r.respond_to(req)
|
None => return Outcome::forward(data, Status::NotFound),
|
||||||
.or_forward((data, Status::InternalServerError))
|
};
|
||||||
},
|
|
||||||
None => Outcome::forward(data, Status::NotFound),
|
outcome.or_forward((data, status))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::export! {
|
impl fmt::Debug for FileServer {
|
||||||
/// Generates a crate-relative version of a path.
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
///
|
f.debug_struct("FileServer")
|
||||||
/// This macro is primarily intended for use with [`FileServer`] to serve
|
.field("rewrites", &Formatter(|f| write!(f, "<{} rewrites>", self.rewrites.len())))
|
||||||
/// files from a path relative to the crate root.
|
.field("rank", &self.rank)
|
||||||
///
|
.finish()
|
||||||
/// The macro accepts one parameter, `$path`, an absolute or (preferably)
|
}
|
||||||
/// relative path. It returns a path as an `&'static str` prefixed with the
|
}
|
||||||
/// path to the crate root. Use `Path::new(relative!($path))` to retrieve an
|
|
||||||
/// `&'static Path`.
|
impl<'r> File<'r> {
|
||||||
///
|
async fn open(self) -> std::io::Result<NamedFile<'r>> {
|
||||||
/// # Example
|
let file = tokio::fs::File::open(&self.path).await?;
|
||||||
///
|
let metadata = file.metadata().await?;
|
||||||
/// Serve files from the crate-relative `static/` directory:
|
if metadata.is_dir() {
|
||||||
///
|
return Err(std::io::Error::other("is a directory"));
|
||||||
/// ```rust
|
}
|
||||||
/// # #[macro_use] extern crate rocket;
|
|
||||||
/// use rocket::fs::{FileServer, relative};
|
Ok(NamedFile {
|
||||||
///
|
file,
|
||||||
/// #[launch]
|
len: metadata.len(),
|
||||||
/// fn rocket() -> _ {
|
path: self.path,
|
||||||
/// rocket::build().mount("/", FileServer::from(relative!("static")))
|
headers: self.headers,
|
||||||
/// }
|
})
|
||||||
/// ```
|
}
|
||||||
///
|
}
|
||||||
/// Path equivalences:
|
|
||||||
///
|
struct NamedFile<'r> {
|
||||||
/// ```rust
|
file: tokio::fs::File,
|
||||||
/// use std::path::Path;
|
len: u64,
|
||||||
///
|
path: Cow<'r, Path>,
|
||||||
/// use rocket::fs::relative;
|
headers: HeaderMap<'r>,
|
||||||
///
|
}
|
||||||
/// let manual = Path::new(env!("CARGO_MANIFEST_DIR")).join("static");
|
|
||||||
/// let automatic_1 = Path::new(relative!("static"));
|
// Do we want to allow the user to rewrite the Content-Type?
|
||||||
/// let automatic_2 = Path::new(relative!("/static"));
|
impl<'r> Responder<'r, 'r> for NamedFile<'r> {
|
||||||
/// assert_eq!(manual, automatic_1);
|
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'r> {
|
||||||
/// assert_eq!(automatic_1, automatic_2);
|
let mut response = Response::new();
|
||||||
/// ```
|
response.set_header_map(self.headers);
|
||||||
///
|
if !response.headers().contains("Content-Type") {
|
||||||
macro_rules! relative {
|
self.path.extension()
|
||||||
($path:expr) => {
|
.and_then(|ext| ext.to_str())
|
||||||
if cfg!(windows) {
|
.and_then(ContentType::from_extension)
|
||||||
concat!(env!("CARGO_MANIFEST_DIR"), "\\", $path)
|
.map(|content_type| response.set_header(content_type));
|
||||||
} else {
|
}
|
||||||
concat!(env!("CARGO_MANIFEST_DIR"), "/", $path)
|
|
||||||
}
|
response.set_sized_body(self.len as usize, self.file);
|
||||||
};
|
Ok(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ use crate::http::Status;
|
||||||
///
|
///
|
||||||
/// [`Origin`]: crate::http::uri::Origin
|
/// [`Origin`]: crate::http::uri::Origin
|
||||||
/// [`uri!`]: ../macro.uri.html
|
/// [`uri!`]: ../macro.uri.html
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Redirect(Status, Option<Reference<'static>>);
|
pub struct Redirect(Status, Option<Reference<'static>>);
|
||||||
|
|
||||||
impl Redirect {
|
impl Redirect {
|
||||||
|
|
|
@ -637,6 +637,10 @@ impl<'r> Response<'r> {
|
||||||
&self.headers
|
&self.headers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_header_map<'h: 'r>(&mut self, headers: HeaderMap<'h>) {
|
||||||
|
self.headers = headers;
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the header `header` in `self`. Any existing headers with the name
|
/// Sets the header `header` in `self`. Any existing headers with the name
|
||||||
/// `header.name` will be lost, and only `header` will remain. The type of
|
/// `header.name` will be lost, and only `header` will remain. The type of
|
||||||
/// `header` can be any type that implements `Into<Header>`. See [trait
|
/// `header` can be any type that implements `Into<Header>`. See [trait
|
||||||
|
|
|
@ -1,19 +1,10 @@
|
||||||
use std::{io::Read, fs::File};
|
use std::{io::Read, fs};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use rocket::{Rocket, Route, Build};
|
use rocket::{Rocket, Route, Build};
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
use rocket::local::blocking::Client;
|
use rocket::local::blocking::Client;
|
||||||
use rocket::fs::{
|
use rocket::fs::{FileServer, relative, rewrite::*};
|
||||||
dir_root,
|
|
||||||
file_root,
|
|
||||||
filter_dotfiles,
|
|
||||||
index,
|
|
||||||
file_root_permissive,
|
|
||||||
normalize_dirs,
|
|
||||||
relative,
|
|
||||||
FileServer
|
|
||||||
};
|
|
||||||
|
|
||||||
fn static_root() -> &'static Path {
|
fn static_root() -> &'static Path {
|
||||||
Path::new(relative!("/tests/static"))
|
Path::new(relative!("/tests/static"))
|
||||||
|
@ -22,57 +13,65 @@ fn static_root() -> &'static Path {
|
||||||
fn rocket() -> Rocket<Build> {
|
fn rocket() -> Rocket<Build> {
|
||||||
let root = static_root();
|
let root = static_root();
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.mount("/default", FileServer::from(&root))
|
.mount("/default", FileServer::new(&root))
|
||||||
.mount(
|
.mount(
|
||||||
"/no_index",
|
"/no_index",
|
||||||
FileServer::empty()
|
FileServer::identity()
|
||||||
.filter_file(filter_dotfiles)
|
.filter(|f, _| f.is_visible())
|
||||||
.map_file(dir_root(&root))
|
.rewrite(Prefix::checked(&root))
|
||||||
)
|
)
|
||||||
.mount(
|
.mount(
|
||||||
"/dots",
|
"/dots",
|
||||||
FileServer::empty()
|
FileServer::identity()
|
||||||
.map_file(dir_root(&root))
|
.rewrite(Prefix::checked(&root))
|
||||||
)
|
)
|
||||||
.mount(
|
.mount(
|
||||||
"/index",
|
"/index",
|
||||||
FileServer::empty()
|
FileServer::identity()
|
||||||
.filter_file(filter_dotfiles)
|
.filter(|f, _| f.is_visible())
|
||||||
.map_file(dir_root(&root))
|
.rewrite(Prefix::checked(&root))
|
||||||
.map_file(index("index.html"))
|
.rewrite(DirIndex::unconditional("index.html"))
|
||||||
|
)
|
||||||
|
.mount(
|
||||||
|
"/try_index",
|
||||||
|
FileServer::identity()
|
||||||
|
.filter(|f, _| f.is_visible())
|
||||||
|
.rewrite(Prefix::checked(&root))
|
||||||
|
.rewrite(DirIndex::if_exists("index.html"))
|
||||||
|
.rewrite(DirIndex::if_exists("index.htm"))
|
||||||
)
|
)
|
||||||
.mount(
|
.mount(
|
||||||
"/both",
|
"/both",
|
||||||
FileServer::empty()
|
FileServer::identity()
|
||||||
.map_file(dir_root(&root))
|
.rewrite(Prefix::checked(&root))
|
||||||
.map_file(index("index.html"))
|
.rewrite(DirIndex::unconditional("index.html"))
|
||||||
)
|
)
|
||||||
.mount(
|
.mount(
|
||||||
"/redir",
|
"/redir",
|
||||||
FileServer::empty()
|
FileServer::identity()
|
||||||
.filter_file(filter_dotfiles)
|
.filter(|f, _| f.is_visible())
|
||||||
.map_file(dir_root(&root))
|
.rewrite(Prefix::checked(&root))
|
||||||
.map_file(normalize_dirs)
|
.rewrite(TrailingDirs)
|
||||||
)
|
)
|
||||||
.mount(
|
.mount(
|
||||||
"/redir_index",
|
"/redir_index",
|
||||||
FileServer::empty()
|
FileServer::identity()
|
||||||
.filter_file(filter_dotfiles)
|
.filter(|f, _| f.is_visible())
|
||||||
.map_file(dir_root(&root))
|
.rewrite(Prefix::checked(&root))
|
||||||
.map_file(normalize_dirs)
|
.rewrite(TrailingDirs)
|
||||||
.map_file(index("index.html"))
|
.rewrite(DirIndex::unconditional("index.html"))
|
||||||
)
|
)
|
||||||
.mount(
|
.mount(
|
||||||
"/index_file",
|
"/index_file",
|
||||||
FileServer::empty()
|
FileServer::identity()
|
||||||
.filter_file(filter_dotfiles)
|
.filter(|f, _| f.is_visible())
|
||||||
.map_file(file_root(root.join("other/hello.txt")))
|
.rewrite(File::checked(root.join("other/hello.txt")))
|
||||||
)
|
)
|
||||||
.mount(
|
.mount(
|
||||||
"/missing_root",
|
"/missing_root",
|
||||||
FileServer::empty()
|
FileServer::identity()
|
||||||
.filter_file(filter_dotfiles)
|
.filter(|f, _| f.is_visible())
|
||||||
.map_file(file_root_permissive(root.join("no_file")))
|
.rewrite(File::new(root.join("no_file")))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,6 +80,7 @@ static REGULAR_FILES: &[&str] = &[
|
||||||
"inner/goodbye",
|
"inner/goodbye",
|
||||||
"inner/index.html",
|
"inner/index.html",
|
||||||
"other/hello.txt",
|
"other/hello.txt",
|
||||||
|
"other/index.htm",
|
||||||
];
|
];
|
||||||
|
|
||||||
static HIDDEN_FILES: &[&str] = &[
|
static HIDDEN_FILES: &[&str] = &[
|
||||||
|
@ -104,7 +104,7 @@ fn assert_file_matches(client: &Client, prefix: &str, path: &str, disk_path: Opt
|
||||||
path = path.join("index.html");
|
path = path.join("index.html");
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut file = File::open(path).expect("open file");
|
let mut file = fs::File::open(path).expect("open file");
|
||||||
let mut expected_contents = String::new();
|
let mut expected_contents = String::new();
|
||||||
file.read_to_string(&mut expected_contents).expect("read file");
|
file.read_to_string(&mut expected_contents).expect("read file");
|
||||||
assert_eq!(response.into_string(), Some(expected_contents));
|
assert_eq!(response.into_string(), Some(expected_contents));
|
||||||
|
@ -179,12 +179,19 @@ fn test_allow_special_dotpaths() {
|
||||||
assert_file_matches(&client, "no_index", "../index.html", Some("index.html"));
|
assert_file_matches(&client, "no_index", "../index.html", Some("index.html"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_try_index() {
|
||||||
|
let client = Client::debug(rocket()).expect("valid rocket");
|
||||||
|
assert_file_matches(&client, "try_index", "inner", Some("inner/index.html"));
|
||||||
|
assert_file_matches(&client, "try_index", "other", Some("other/index.htm"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ranking() {
|
fn test_ranking() {
|
||||||
let root = static_root();
|
let root = static_root();
|
||||||
for rank in -128..128 {
|
for rank in -128..128 {
|
||||||
let a = FileServer::new(&root, rank);
|
let a = FileServer::new(&root).rank(rank);
|
||||||
let b = FileServer::new(&root, rank);
|
let b = FileServer::new(&root).rank(rank);
|
||||||
|
|
||||||
for handler in vec![a, b] {
|
for handler in vec![a, b] {
|
||||||
let routes: Vec<Route> = handler.into();
|
let routes: Vec<Route> = handler.into();
|
||||||
|
@ -231,15 +238,15 @@ fn test_redirection() {
|
||||||
assert_eq!(response.status(), Status::Ok);
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
|
||||||
let response = client.get("/redir/inner").dispatch();
|
let response = client.get("/redir/inner").dispatch();
|
||||||
assert_eq!(response.status(), Status::PermanentRedirect);
|
assert_eq!(response.status(), Status::TemporaryRedirect);
|
||||||
assert_eq!(response.headers().get("Location").next(), Some("/redir/inner/"));
|
assert_eq!(response.headers().get("Location").next(), Some("/redir/inner/"));
|
||||||
|
|
||||||
let response = client.get("/redir/inner?foo=bar").dispatch();
|
let response = client.get("/redir/inner?foo=bar").dispatch();
|
||||||
assert_eq!(response.status(), Status::PermanentRedirect);
|
assert_eq!(response.status(), Status::TemporaryRedirect);
|
||||||
assert_eq!(response.headers().get("Location").next(), Some("/redir/inner/?foo=bar"));
|
assert_eq!(response.headers().get("Location").next(), Some("/redir/inner/?foo=bar"));
|
||||||
|
|
||||||
let response = client.get("/redir_index/inner").dispatch();
|
let response = client.get("/redir_index/inner").dispatch();
|
||||||
assert_eq!(response.status(), Status::PermanentRedirect);
|
assert_eq!(response.status(), Status::TemporaryRedirect);
|
||||||
assert_eq!(response.headers().get("Location").next(), Some("/redir_index/inner/"));
|
assert_eq!(response.headers().get("Location").next(), Some("/redir_index/inner/"));
|
||||||
|
|
||||||
// Paths with trailing slash are unaffected.
|
// Paths with trailing slash are unaffected.
|
||||||
|
@ -257,32 +264,32 @@ fn test_redirection() {
|
||||||
assert_eq!(response.status(), Status::Ok);
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
|
||||||
let response = client.get("/redir/inner").dispatch();
|
let response = client.get("/redir/inner").dispatch();
|
||||||
assert_eq!(response.status(), Status::PermanentRedirect);
|
assert_eq!(response.status(), Status::TemporaryRedirect);
|
||||||
assert_eq!(response.headers().get("Location").next(), Some("/redir/inner/"));
|
assert_eq!(response.headers().get("Location").next(), Some("/redir/inner/"));
|
||||||
|
|
||||||
let response = client.get("/redir/other").dispatch();
|
let response = client.get("/redir/other").dispatch();
|
||||||
assert_eq!(response.status(), Status::PermanentRedirect);
|
assert_eq!(response.status(), Status::TemporaryRedirect);
|
||||||
assert_eq!(response.headers().get("Location").next(), Some("/redir/other/"));
|
assert_eq!(response.headers().get("Location").next(), Some("/redir/other/"));
|
||||||
|
|
||||||
let response = client.get("/redir_index/other").dispatch();
|
let response = client.get("/redir_index/other").dispatch();
|
||||||
assert_eq!(response.status(), Status::PermanentRedirect);
|
assert_eq!(response.status(), Status::TemporaryRedirect);
|
||||||
assert_eq!(response.headers().get("Location").next(), Some("/redir_index/other/"));
|
assert_eq!(response.headers().get("Location").next(), Some("/redir_index/other/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic]
|
#[should_panic]
|
||||||
fn test_panic_on_missing_file() {
|
fn test_panic_on_missing_file() {
|
||||||
let _ = file_root(static_root().join("missing_file"));
|
let _ = File::checked(static_root().join("missing_file"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic]
|
#[should_panic]
|
||||||
fn test_panic_on_missing_dir() {
|
fn test_panic_on_missing_dir() {
|
||||||
let _ = dir_root(static_root().join("missing_dir"));
|
let _ = Prefix::checked(static_root().join("missing_dir"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic]
|
#[should_panic]
|
||||||
fn test_panic_on_file_not_dir() {
|
fn test_panic_on_file_not_dir() {
|
||||||
let _ = dir_root(static_root().join("index.html"));
|
let _ = Prefix::checked(static_root().join("index.html"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Inner index.
|
Inner index.html
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Inner index.htm
|
|
@ -169,7 +169,7 @@ async fn files(file: PathBuf) -> Option<NamedFile> {
|
||||||
fn rocket() -> _ {
|
fn rocket() -> _ {
|
||||||
rocket::build()
|
rocket::build()
|
||||||
// serve files from `/www/static` at path `/public`
|
// serve files from `/www/static` at path `/public`
|
||||||
.mount("/public", FileServer::from("/www/static"))
|
.mount("/public", FileServer::new("/www/static"))
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ For any deployment, it's important to keep in mind:
|
||||||
#[launch]
|
#[launch]
|
||||||
fn rocket() -> _ {
|
fn rocket() -> _ {
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.mount("/", FileServer::from("./static"))
|
.mount("/", FileServer::new("./static"))
|
||||||
.attach(Template::fairing())
|
.attach(Template::fairing())
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
@ -54,5 +54,5 @@ fn rocket() -> _ {
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.manage(channel::<Message>(1024).0)
|
.manage(channel::<Message>(1024).0)
|
||||||
.mount("/", routes![post, events])
|
.mount("/", routes![post, events])
|
||||||
.mount("/", FileServer::from(relative!("static")))
|
.mount("/", FileServer::new(relative!("static")))
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,5 +93,5 @@ fn rocket() -> _ {
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.mount("/", routes![index, submit])
|
.mount("/", routes![index, submit])
|
||||||
.attach(Template::fairing())
|
.attach(Template::fairing())
|
||||||
.mount("/", FileServer::from(relative!("/static")))
|
.mount("/", FileServer::new(relative!("/static")))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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("/", FileServer::from(relative!("static")))
|
.mount("/", FileServer::new(relative!("static")))
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ fn test_icon_file() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_path() {
|
fn test_invalid_path() {
|
||||||
test_query_file("/hidden", None, Status::PermanentRedirect);
|
test_query_file("/hidden", None, Status::TemporaryRedirect);
|
||||||
test_query_file("/thou_shalt_not_exist", None, Status::NotFound);
|
test_query_file("/thou_shalt_not_exist", None, Status::NotFound);
|
||||||
test_query_file("/thou/shalt/not/exist", None, Status::NotFound);
|
test_query_file("/thou/shalt/not/exist", None, Status::NotFound);
|
||||||
test_query_file("/thou/shalt/not/exist?a=b&c=d", None, Status::NotFound);
|
test_query_file("/thou/shalt/not/exist?a=b&c=d", None, Status::NotFound);
|
||||||
|
|
|
@ -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("/", FileServer::from(relative!("static")))
|
.mount("/", FileServer::new(relative!("static")))
|
||||||
.mount("/", routes![index])
|
.mount("/", routes![index])
|
||||||
.mount("/todo", routes![new, toggle, delete])
|
.mount("/todo", routes![new, toggle, delete])
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,5 +39,5 @@ fn echo_raw(ws: ws::WebSocket) -> ws::Stream!['static] {
|
||||||
fn rocket() -> _ {
|
fn rocket() -> _ {
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.mount("/", routes![echo_channel, echo_stream, echo_raw])
|
.mount("/", routes![echo_channel, echo_stream, echo_raw])
|
||||||
.mount("/", FileServer::from(fs::relative!("static")))
|
.mount("/", FileServer::new(fs::relative!("static")))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue