mirror of https://github.com/rwf2/Rocket.git
Implement FileServer rewrite API.
Implements the FileServer API proposed in https://github.com/rwf2/Rocket/pull/2716#issuecomment-1930893889. Closes #2716.
This commit is contained in:
parent
fb4b630405
commit
65e3b87d6b
|
@ -218,6 +218,8 @@ impl<'a> Segments<'a, Path> {
|
||||||
for segment in self.clone() {
|
for segment in self.clone() {
|
||||||
if segment == ".." {
|
if segment == ".." {
|
||||||
buf.pop();
|
buf.pop();
|
||||||
|
} else if segment == "." {
|
||||||
|
continue;
|
||||||
} 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('*') {
|
||||||
|
|
|
@ -1,32 +1,46 @@
|
||||||
use std::path::{PathBuf, Path};
|
use core::fmt;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{Request, Data};
|
use crate::fs::NamedFile;
|
||||||
use crate::http::{Method, Status, uri::Segments, ext::IntoOwned};
|
use crate::{Data, Request, outcome::IntoOutcome};
|
||||||
|
use crate::http::{
|
||||||
|
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::{Redirect, Responder};
|
||||||
use crate::outcome::IntoOutcome;
|
|
||||||
use crate::fs::NamedFile;
|
|
||||||
|
|
||||||
/// Custom handler for serving static files.
|
/// Custom handler for serving static files.
|
||||||
///
|
///
|
||||||
/// This handler makes it 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 either
|
/// local file system. To use it, construct a `FileServer` using
|
||||||
/// [`FileServer::from()`] or [`FileServer::new()`] then simply `mount` the
|
/// [`FileServer::from()`], then simply `mount` the handler. When mounted, the
|
||||||
/// handler at a desired path. When mounted, the handler will generate route(s)
|
/// handler serves files from the specified directory. If the file is not found,
|
||||||
/// that serve the desired static files. If a requested file is not found, the
|
/// the handler _forwards_ the request. By default, `FileServer` has a rank of
|
||||||
/// routes _forward_ the incoming request. The default rank of the generated
|
/// `10`. Use [`FileServer::new()`] to create a route with a custom rank.
|
||||||
/// routes is `10`. To customize route ranking, use the [`FileServer::rank()`]
|
|
||||||
/// method.
|
|
||||||
///
|
///
|
||||||
/// # Options
|
/// # Customization
|
||||||
///
|
///
|
||||||
/// The handler's functionality can be customized by passing an [`Options`] to
|
/// How `FileServer` responds to specific requests can be customized, through
|
||||||
/// [`FileServer::new()`].
|
/// 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
|
/// # Example
|
||||||
///
|
///
|
||||||
/// Serve files from the `/static` directory on the local file system at the
|
/// Serve files from the `/static` directory on the local file system at the
|
||||||
/// `/public` path with the [default options](#impl-Default):
|
/// `/public` path, with the default rewrites.
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # #[macro_use] extern crate rocket;
|
/// # #[macro_use] extern crate rocket;
|
||||||
|
@ -39,9 +53,8 @@ use crate::fs::NamedFile;
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// Requests for files at `/public/<path..>` will be handled by returning the
|
/// Requests for files at `/public/<path..>` will be handled by returning the
|
||||||
/// contents of `/static/<path..>`. Requests for _directories_ at
|
/// contents of `/static/<path..>`. Requests for directories will return the
|
||||||
/// `/public/<directory>` will be handled by returning the contents of
|
/// contents of `index.html`.
|
||||||
/// `/static/<directory>/index.html`.
|
|
||||||
///
|
///
|
||||||
/// ## Relative Paths
|
/// ## Relative Paths
|
||||||
///
|
///
|
||||||
|
@ -60,354 +73,540 @@ use crate::fs::NamedFile;
|
||||||
/// rocket::build().mount("/", FileServer::from(relative!("static")))
|
/// rocket::build().mount("/", FileServer::from(relative!("static")))
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FileServer {
|
pub struct FileServer {
|
||||||
root: PathBuf,
|
rewrites: Vec<Arc<dyn Rewriter>>,
|
||||||
options: Options,
|
|
||||||
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` that serves files from the file system
|
/// Constructs a new `FileServer`, with default rank, and no
|
||||||
/// `path`. By default, [`Options::Index`] is set, and the generated routes
|
/// rewrites.
|
||||||
/// have a rank of `10`. To serve static files with other options, use
|
|
||||||
/// [`FileServer::new()`]. To choose a different rank for generated routes,
|
|
||||||
/// use [`FileServer::rank()`].
|
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// See [`FileServer::empty_ranked()`].
|
||||||
///
|
pub fn empty() -> Self {
|
||||||
/// Panics if `path` does not exist or is not a directory.
|
Self::empty_ranked(Self::DEFAULT_RANK)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a new `FileServer`, with specified rank, and no
|
||||||
|
/// rewrites.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// Serve the static files in the `/www/public` local directory on path
|
/// Replicate the output of [`FileServer::new()`].
|
||||||
/// `/static`.
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # #[macro_use] extern crate rocket;
|
/// # use rocket::fs::{FileServer, filter_dotfiles, dir_root, normalize_dirs};
|
||||||
/// use rocket::fs::FileServer;
|
/// # fn launch() -> FileServer {
|
||||||
///
|
/// FileServer::empty_ranked(10)
|
||||||
/// #[launch]
|
/// .filter_file(filter_dotfiles)
|
||||||
/// fn rocket() -> _ {
|
/// .map_file(dir_root("static"))
|
||||||
/// rocket::build().mount("/static", FileServer::from("/www/public"))
|
/// .map_file(normalize_dirs)
|
||||||
/// }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
|
pub fn empty_ranked(rank: isize) -> Self {
|
||||||
|
Self {
|
||||||
|
rewrites: vec![],
|
||||||
|
rank,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a new `FileServer`, with the defualt rank of 10.
|
||||||
///
|
///
|
||||||
/// Exactly as before, but set the rank for generated routes to `30`.
|
/// See [`FileServer::new`].
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// # #[macro_use] extern crate rocket;
|
|
||||||
/// use rocket::fs::FileServer;
|
|
||||||
///
|
|
||||||
/// #[launch]
|
|
||||||
/// fn rocket() -> _ {
|
|
||||||
/// rocket::build().mount("/static", FileServer::from("/www/public").rank(30))
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
#[track_caller]
|
|
||||||
pub fn from<P: AsRef<Path>>(path: P) -> Self {
|
pub fn from<P: AsRef<Path>>(path: P) -> Self {
|
||||||
FileServer::new(path, Options::default())
|
Self::new(path, Self::DEFAULT_RANK)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a new `FileServer` that serves files from the file system
|
/// Constructs a new `FileServer` that serves files from the file system
|
||||||
/// `path` with `options` enabled. By default, the handler's routes have a
|
/// `path`, with the specified rank.
|
||||||
/// rank of `10`. To choose a different rank, use [`FileServer::rank()`].
|
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// Adds a set of default rewrites:
|
||||||
///
|
/// - [`filter_dotfiles`]: Hides all dotfiles.
|
||||||
/// If [`Options::Missing`] is not set, panics if `path` does not exist or
|
/// - [`dir_root(path)`](dir_root): Applies the root path.
|
||||||
/// is not a directory. Otherwise does not panic.
|
/// - [`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
|
||||||
///
|
///
|
||||||
/// Serve the static files in the `/www/public` local directory on path
|
/// Redirects all requests that have been filtered to the root of the `FileServer`.
|
||||||
/// `/static` without serving index files or dot files. Additionally, serve
|
|
||||||
/// the same files on `/pub` with a route rank of -1 while also serving
|
|
||||||
/// index files and dot files.
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// # #[macro_use] extern crate rocket;
|
/// # use rocket::{fs::{FileServer, FileResponse}, response::Redirect,
|
||||||
/// use rocket::fs::{FileServer, Options};
|
/// # uri, Build, Rocket, Request};
|
||||||
///
|
/// fn redir_missing<'p, 'h>(p: Option<FileResponse<'p, 'h>>, _req: &Request<'_>)
|
||||||
/// #[launch]
|
/// -> Option<FileResponse<'p, 'h>>
|
||||||
/// fn rocket() -> _ {
|
/// {
|
||||||
/// let options = Options::Index | Options::DotFiles;
|
/// match p {
|
||||||
/// rocket::build()
|
/// None => Redirect::temporary(uri!("/")).into(),
|
||||||
/// .mount("/static", FileServer::from("/www/public"))
|
/// p => p,
|
||||||
/// .mount("/pub", FileServer::new("/www/public", options).rank(-1))
|
|
||||||
/// }
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// # fn launch() -> Rocket<Build> {
|
||||||
|
/// rocket::build()
|
||||||
|
/// .mount("/", FileServer::from("static").and_rewrite(redir_missing))
|
||||||
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
#[track_caller]
|
///
|
||||||
pub fn new<P: AsRef<Path>>(path: P, options: Options) -> Self {
|
/// Note that `redir_missing` is not a closure in this example. Making it a closure
|
||||||
let path = path.as_ref();
|
/// causes compilation to fail with a lifetime error. It really shouldn't but it does.
|
||||||
if !options.contains(Options::Missing) {
|
pub fn and_rewrite(mut self, f: impl Rewriter) -> Self {
|
||||||
if !options.contains(Options::IndexFile) && !path.is_dir() {
|
self.rewrites.push(Arc::new(f));
|
||||||
error!(path = %path.display(),
|
self
|
||||||
"FileServer path does not point to a directory.\n\
|
|
||||||
Aborting early to prevent inevitable handler runtime errors.");
|
|
||||||
|
|
||||||
panic!("invalid directory path: refusing to continue");
|
|
||||||
} else if !path.exists() {
|
|
||||||
error!(path = %path.display(),
|
|
||||||
"FileServer path does not point to a file.\n\
|
|
||||||
Aborting early to prevent inevitable handler runtime errors.");
|
|
||||||
|
|
||||||
panic!("invalid file path: refusing to continue");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FileServer { root: path.into(), options, rank: Self::DEFAULT_RANK }
|
/// Filter what files this `FileServer` will respond with
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the rank for generated routes to `rank`.
|
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
|
/// Filter out all paths with a filename of `hidden`.
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
/// use rocket::fs::{FileServer, Options};
|
/// # use rocket::{fs::FileServer, response::Redirect, uri, Rocket, Build};
|
||||||
///
|
/// # fn launch() -> Rocket<Build> {
|
||||||
/// // A `FileServer` created with `from()` with routes of rank `3`.
|
/// rocket::build()
|
||||||
/// FileServer::from("/public").rank(3);
|
/// .mount(
|
||||||
///
|
/// "/",
|
||||||
/// // A `FileServer` created with `new()` with routes of rank `-15`.
|
/// FileServer::from("static")
|
||||||
/// FileServer::new("/public", Options::Index).rank(-15);
|
/// .filter_file(|f, _r| f.path.file_name() != Some("hidden".as_ref()))
|
||||||
|
/// )
|
||||||
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn rank(mut self, rank: isize) -> Self {
|
pub fn filter_file<F>(self, f: F) -> Self
|
||||||
self.rank = rank;
|
where F: Fn(&File<'_, '_>, &Request<'_>) -> bool + Send + Sync + 'static
|
||||||
self
|
{
|
||||||
|
self.and_rewrite(FilterFile(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transform files
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// Append `hidden` to the path of every file returned.
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # use rocket::{fs::FileServer, Build, Rocket};
|
||||||
|
/// # fn launch() -> Rocket<Build> {
|
||||||
|
/// rocket::build()
|
||||||
|
/// .mount(
|
||||||
|
/// "/",
|
||||||
|
/// FileServer::from("static")
|
||||||
|
/// .map_file(|f, _r| f.map_path(|p| p.join("hidden")).into())
|
||||||
|
/// )
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn map_file<F>(self, f: F) -> Self
|
||||||
|
where F: for<'r, 'h> Fn(File<'r, 'h>, &Request<'_>)
|
||||||
|
-> FileResponse<'r, 'h> + Send + Sync + 'static
|
||||||
|
{
|
||||||
|
self.and_rewrite(MapFile(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 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);
|
||||||
route.name = Some(format!("FileServer: {}", source).into());
|
// 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());
|
||||||
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;
|
use crate::http::uri::fmt::Path as UriPath;
|
||||||
|
let path: Option<PathBuf> = req.segments::<Segments<'_, UriPath>>(0..).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(),
|
||||||
|
}));
|
||||||
|
|
||||||
// TODO: Should we reject dotfiles for `self.root` if !DotFiles?
|
for rewrite in &self.rewrites {
|
||||||
let options = self.options;
|
response = rewrite.rewrite(response, req);
|
||||||
if options.contains(Options::IndexFile) && self.root.is_file() {
|
|
||||||
let segments = match req.segments::<Segments<'_, Path>>(0..) {
|
|
||||||
Ok(segments) => segments,
|
|
||||||
Err(never) => match never {},
|
|
||||||
};
|
|
||||||
|
|
||||||
if segments.is_empty() {
|
|
||||||
let file = NamedFile::open(&self.root).await;
|
|
||||||
return file.respond_to(req).or_forward((data, Status::NotFound));
|
|
||||||
} else {
|
|
||||||
return Outcome::forward(data, Status::NotFound);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the segments as a `PathBuf`, allowing dotfiles requested.
|
match response {
|
||||||
let allow_dotfiles = options.contains(Options::DotFiles);
|
Some(FileResponse::File(file)) => file.respond_to(req, data).await,
|
||||||
let path = req.segments::<Segments<'_, Path>>(0..).ok()
|
Some(FileResponse::Redirect(r)) => {
|
||||||
.and_then(|segments| segments.to_path_buf(allow_dotfiles).ok())
|
r.respond_to(req)
|
||||||
.map(|path| self.root.join(path));
|
.or_forward((data, Status::InternalServerError))
|
||||||
|
|
||||||
match path {
|
|
||||||
Some(p) if p.is_dir() => {
|
|
||||||
// Normalize '/a/b/foo' to '/a/b/foo/'.
|
|
||||||
if options.contains(Options::NormalizeDirs) && !req.uri().path().ends_with('/') {
|
|
||||||
let normal = req.uri().map_path(|p| format!("{}/", p))
|
|
||||||
.expect("adding a trailing slash to a known good path => valid path")
|
|
||||||
.into_owned();
|
|
||||||
|
|
||||||
return Redirect::permanent(normal)
|
|
||||||
.respond_to(req)
|
|
||||||
.or_forward((data, Status::InternalServerError));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !options.contains(Options::Index) {
|
|
||||||
return Outcome::forward(data, Status::NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
let index = NamedFile::open(p.join("index.html")).await;
|
|
||||||
index.respond_to(req).or_forward((data, Status::NotFound))
|
|
||||||
},
|
},
|
||||||
Some(p) => {
|
|
||||||
let file = NamedFile::open(p).await;
|
|
||||||
file.respond_to(req).or_forward((data, Status::NotFound))
|
|
||||||
}
|
|
||||||
None => Outcome::forward(data, Status::NotFound),
|
None => Outcome::forward(data, Status::NotFound),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A bitset representing configurable options for [`FileServer`].
|
|
||||||
///
|
|
||||||
/// 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.
|
|
||||||
/// * [`Options::IndexFile`] - Allow serving a single file as the index.
|
|
||||||
/// * [`Options::Missing`] - Don't fail if the path to serve is missing.
|
|
||||||
/// * [`Options::NormalizeDirs`] - Redirect directories without a trailing
|
|
||||||
/// slash to ones with a trailing slash.
|
|
||||||
///
|
|
||||||
/// `Options` structures can be `or`d together to select 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, non_snake_case)]
|
|
||||||
impl Options {
|
|
||||||
/// All options disabled.
|
|
||||||
///
|
|
||||||
/// Note that this is different than [`Options::default()`](#impl-Default),
|
|
||||||
/// which enables options.
|
|
||||||
pub const None: Options = Options(0);
|
|
||||||
|
|
||||||
/// Respond to requests for a directory with the `index.html` file in that
|
|
||||||
/// directory, if it exists.
|
|
||||||
///
|
|
||||||
/// When enabled, [`FileServer`] will respond to requests for a directory
|
|
||||||
/// `/foo` or `/foo/` with the file at `${root}/foo/index.html` if it
|
|
||||||
/// exists. When disabled, requests to directories will always forward.
|
|
||||||
///
|
|
||||||
/// **Enabled by default.**
|
|
||||||
pub const Index: Options = Options(1 << 0);
|
|
||||||
|
|
||||||
/// Allow serving dotfiles.
|
|
||||||
///
|
|
||||||
/// When enabled, [`FileServer`] will respond to requests for files or
|
|
||||||
/// directories beginning with `.`. When disabled, any dotfiles will be
|
|
||||||
/// treated as missing.
|
|
||||||
///
|
|
||||||
/// **Disabled by default.**
|
|
||||||
pub const DotFiles: Options = Options(1 << 1);
|
|
||||||
|
|
||||||
/// Normalizes directory requests by redirecting requests to directory paths
|
|
||||||
/// without a trailing slash to ones with a trailing slash.
|
|
||||||
///
|
|
||||||
/// **Enabled by default.**
|
|
||||||
///
|
|
||||||
/// When enabled, the [`FileServer`] handler will respond to requests for a
|
|
||||||
/// directory without a trailing `/` with a permanent redirect (308) to the
|
|
||||||
/// same path with a trailing `/`. This ensures relative URLs within any
|
|
||||||
/// document served from that directory will be interpreted relative to that
|
|
||||||
/// directory rather than its parent.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// Given the following directory structure...
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// static/
|
|
||||||
/// └── foo/
|
|
||||||
/// ├── cat.jpeg
|
|
||||||
/// └── index.html
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// And the following server:
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// rocket.mount("/", FileServer::from("static"))
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ...requests to `example.com/foo` will be redirected to
|
|
||||||
/// `example.com/foo/`. If `index.html` references `cat.jpeg` as a relative
|
|
||||||
/// URL, the browser will resolve the URL to `example.com/foo/cat.jpeg`,
|
|
||||||
/// which in-turn Rocket will match to `/static/foo/cat.jpg`.
|
|
||||||
///
|
|
||||||
/// Without this option, requests to `example.com/foo` would not be
|
|
||||||
/// redirected. `index.html` would be rendered, and the relative link to
|
|
||||||
/// `cat.jpeg` would be resolved by the browser as `example.com/cat.jpeg`.
|
|
||||||
/// Rocket would thus try to find `/static/cat.jpeg`, which does not exist.
|
|
||||||
pub const NormalizeDirs: Options = Options(1 << 2);
|
|
||||||
|
|
||||||
/// Allow serving a file instead of a directory.
|
|
||||||
///
|
|
||||||
/// By default, `FileServer` will error on construction if the path to serve
|
|
||||||
/// does not point to a directory. When this option is enabled, if a path to
|
|
||||||
/// a file is provided, `FileServer` will serve the file as the root of the
|
|
||||||
/// mount path.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// If the file tree looks like:
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// static/
|
|
||||||
/// └── cat.jpeg
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Then `cat.jpeg` can be served at `/cat` with:
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// # #[macro_use] extern crate rocket;
|
|
||||||
/// use rocket::fs::{FileServer, Options};
|
|
||||||
///
|
|
||||||
/// #[launch]
|
|
||||||
/// fn rocket() -> _ {
|
|
||||||
/// rocket::build()
|
|
||||||
/// .mount("/cat", FileServer::new("static/cat.jpeg", Options::IndexFile))
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub const IndexFile: Options = Options(1 << 3);
|
|
||||||
|
|
||||||
/// Don't fail if the file or directory to serve is missing.
|
|
||||||
///
|
|
||||||
/// By default, `FileServer` will error if the path to serve is missing to
|
|
||||||
/// prevent inevitable 404 errors. This option overrides that.
|
|
||||||
pub const Missing: Options = Options(1 << 4);
|
|
||||||
|
|
||||||
/// 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::fs::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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The default set of options: `Options::Index | Options:NormalizeDirs`.
|
|
||||||
impl Default for Options {
|
|
||||||
fn default() -> Self {
|
|
||||||
Options::Index | Options::NormalizeDirs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::BitOr for Options {
|
|
||||||
type Output = Self;
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn bitor(self, rhs: Self) -> Self {
|
|
||||||
Options(self.0 | rhs.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
crate::export! {
|
crate::export! {
|
||||||
/// Generates a crate-relative version of a path.
|
/// Generates a crate-relative version of a path.
|
||||||
///
|
///
|
||||||
|
|
|
@ -144,6 +144,12 @@ impl Redirect {
|
||||||
pub fn moved<U: TryInto<Reference<'static>>>(uri: U) -> Redirect {
|
pub fn moved<U: TryInto<Reference<'static>>>(uri: U) -> Redirect {
|
||||||
Redirect(Status::MovedPermanently, uri.try_into().ok())
|
Redirect(Status::MovedPermanently, uri.try_into().ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn map_uri<U: TryInto<Reference<'static>>>(self, f: impl FnOnce(Reference<'static>) -> U)
|
||||||
|
-> Redirect
|
||||||
|
{
|
||||||
|
Redirect(self.0, self.1.and_then(|p| f(p).try_into().ok()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a response with the appropriate status code and the given URL in
|
/// Constructs a response with the appropriate status code and the given URL in
|
||||||
|
|
|
@ -4,7 +4,16 @@ 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::{FileServer, Options, relative};
|
use rocket::fs::{
|
||||||
|
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"))
|
||||||
|
@ -14,12 +23,57 @@ fn rocket() -> Rocket<Build> {
|
||||||
let root = static_root();
|
let root = static_root();
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.mount("/default", FileServer::from(&root))
|
.mount("/default", FileServer::from(&root))
|
||||||
.mount("/no_index", FileServer::new(&root, Options::None))
|
.mount(
|
||||||
.mount("/dots", FileServer::new(&root, Options::DotFiles))
|
"/no_index",
|
||||||
.mount("/index", FileServer::new(&root, Options::Index))
|
FileServer::empty()
|
||||||
.mount("/both", FileServer::new(&root, Options::DotFiles | Options::Index))
|
.filter_file(filter_dotfiles)
|
||||||
.mount("/redir", FileServer::new(&root, Options::NormalizeDirs))
|
.map_file(dir_root(&root))
|
||||||
.mount("/redir_index", FileServer::new(&root, Options::NormalizeDirs | Options::Index))
|
)
|
||||||
|
.mount(
|
||||||
|
"/dots",
|
||||||
|
FileServer::empty()
|
||||||
|
.map_file(dir_root(&root))
|
||||||
|
)
|
||||||
|
.mount(
|
||||||
|
"/index",
|
||||||
|
FileServer::empty()
|
||||||
|
.filter_file(filter_dotfiles)
|
||||||
|
.map_file(dir_root(&root))
|
||||||
|
.map_file(index("index.html"))
|
||||||
|
)
|
||||||
|
.mount(
|
||||||
|
"/both",
|
||||||
|
FileServer::empty()
|
||||||
|
.map_file(dir_root(&root))
|
||||||
|
.map_file(index("index.html"))
|
||||||
|
)
|
||||||
|
.mount(
|
||||||
|
"/redir",
|
||||||
|
FileServer::empty()
|
||||||
|
.filter_file(filter_dotfiles)
|
||||||
|
.map_file(dir_root(&root))
|
||||||
|
.map_file(normalize_dirs)
|
||||||
|
)
|
||||||
|
.mount(
|
||||||
|
"/redir_index",
|
||||||
|
FileServer::empty()
|
||||||
|
.filter_file(filter_dotfiles)
|
||||||
|
.map_file(dir_root(&root))
|
||||||
|
.map_file(normalize_dirs)
|
||||||
|
.map_file(index("index.html"))
|
||||||
|
)
|
||||||
|
.mount(
|
||||||
|
"/index_file",
|
||||||
|
FileServer::empty()
|
||||||
|
.filter_file(filter_dotfiles)
|
||||||
|
.map_file(file_root(root.join("other/hello.txt")))
|
||||||
|
)
|
||||||
|
.mount(
|
||||||
|
"/missing_root",
|
||||||
|
FileServer::empty()
|
||||||
|
.filter_file(filter_dotfiles)
|
||||||
|
.map_file(file_root_permissive(root.join("no_file")))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static REGULAR_FILES: &[&str] = &[
|
static REGULAR_FILES: &[&str] = &[
|
||||||
|
@ -39,13 +93,13 @@ static INDEXED_DIRECTORIES: &[&str] = &[
|
||||||
"inner/",
|
"inner/",
|
||||||
];
|
];
|
||||||
|
|
||||||
fn assert_file(client: &Client, prefix: &str, path: &str, exists: bool) {
|
fn assert_file_matches(client: &Client, prefix: &str, path: &str, disk_path: Option<&str>) {
|
||||||
let full_path = format!("/{}/{}", prefix, path);
|
let full_path = format!("/{}/{}", prefix, path);
|
||||||
let response = client.get(full_path).dispatch();
|
let response = client.get(full_path).dispatch();
|
||||||
if exists {
|
if let Some(disk_path) = disk_path {
|
||||||
assert_eq!(response.status(), Status::Ok);
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
|
||||||
let mut path = static_root().join(path);
|
let mut path = static_root().join(disk_path);
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
path = path.join("index.html");
|
path = path.join("index.html");
|
||||||
}
|
}
|
||||||
|
@ -59,6 +113,14 @@ fn assert_file(client: &Client, prefix: &str, path: &str, exists: bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn assert_file(client: &Client, prefix: &str, path: &str, exists: bool) {
|
||||||
|
if exists {
|
||||||
|
assert_file_matches(client, prefix, path, Some(path))
|
||||||
|
} else {
|
||||||
|
assert_file_matches(client, prefix, path, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn assert_all(client: &Client, prefix: &str, paths: &[&str], exist: bool) {
|
fn assert_all(client: &Client, prefix: &str, paths: &[&str], exist: bool) {
|
||||||
for path in paths.iter() {
|
for path in paths.iter() {
|
||||||
assert_file(client, prefix, path, exist);
|
assert_file(client, prefix, path, exist);
|
||||||
|
@ -101,12 +163,28 @@ fn test_static_all() {
|
||||||
assert_all(&client, "both", INDEXED_DIRECTORIES, true);
|
assert_all(&client, "both", INDEXED_DIRECTORIES, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_alt_roots() {
|
||||||
|
let client = Client::debug(rocket()).expect("valid rocket");
|
||||||
|
assert_file(&client, "missing_root", "", false);
|
||||||
|
assert_file_matches(&client, "index_file", "", Some("other/hello.txt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_allow_special_dotpaths() {
|
||||||
|
let client = Client::debug(rocket()).expect("valid rocket");
|
||||||
|
assert_file_matches(&client, "no_index", "./index.html", Some("index.html"));
|
||||||
|
assert_file_matches(&client, "no_index", "foo/../index.html", Some("index.html"));
|
||||||
|
assert_file_matches(&client, "no_index", "inner/./index.html", Some("inner/index.html"));
|
||||||
|
assert_file_matches(&client, "no_index", "../index.html", Some("index.html"));
|
||||||
|
}
|
||||||
|
|
||||||
#[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, Options::None).rank(rank);
|
let a = FileServer::new(&root, rank);
|
||||||
let b = FileServer::from(&root).rank(rank);
|
let b = FileServer::new(&root, 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();
|
||||||
|
@ -190,3 +268,21 @@ fn test_redirection() {
|
||||||
assert_eq!(response.status(), Status::PermanentRedirect);
|
assert_eq!(response.status(), Status::PermanentRedirect);
|
||||||
assert_eq!(response.headers().get("Location").next(), Some("/redir_index/other/"));
|
assert_eq!(response.headers().get("Location").next(), Some("/redir_index/other/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_panic_on_missing_file() {
|
||||||
|
let _ = file_root(static_root().join("missing_file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_panic_on_missing_dir() {
|
||||||
|
let _ = dir_root(static_root().join("missing_dir"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_panic_on_file_not_dir() {
|
||||||
|
let _ = dir_root(static_root().join("index.html"));
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue