Merge branch 'master' into encrypt-data-with-secret-key

This commit is contained in:
Vadim Anufriev 2024-07-07 13:02:27 +04:00
commit 5296e3ff66
31 changed files with 980 additions and 442 deletions

View File

@ -84,6 +84,10 @@ jobs:
echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" >> "$env:GITHUB_ENV"
echo "$env:VCPKG_INSTALLATION_ROOT\installed\x64-windows\lib" >> "$env:GITHUB_PATH"
- name: Install NASM (Windows)
if: matrix.platform.name == 'Windows'
uses: ilammy/setup-nasm@v1
- name: Install Native Dependencies (Linux)
if: matrix.platform.name == 'Linux'
run: |

View File

@ -9,11 +9,11 @@
This crate adds support for dynamic template rendering to Rocket. It
automatically discovers templates, provides a `Responder` to render templates,
and automatically reloads templates when compiled in debug mode. At present, it
supports [Handlebars] and [Tera].
and automatically reloads templates when compiled in debug mode. It supports [Handlebars], [Tera] and [MiniJinja].
[Tera]: https://docs.rs/crate/tera/1
[Handlebars]: https://docs.rs/crate/handlebars/5
[MiniJinja]: https://docs.rs/crate/minijinja/2.0.1
# Usage
@ -23,7 +23,7 @@ supports [Handlebars] and [Tera].
```toml
[dependencies.rocket_dyn_templates]
version = "0.1.0"
features = ["handlebars", "tera"]
features = ["handlebars", "tera", "minijinja"]
```
1. Write your template files in Handlebars (`.hbs`) and/or Tera (`.tera`) in

View File

@ -105,9 +105,13 @@ fn query_decls(route: &Route) -> Option<TokenStream> {
)*
if !__e.is_empty() {
::rocket::trace::span_info!("codegen",
::rocket::trace::span_info!(
"codegen",
"query string failed to match route declaration" =>
{ for _err in __e { ::rocket::trace::info!("{_err}"); } }
{ for _err in __e { ::rocket::trace::info!(
target: concat!("rocket::codegen::route::", module_path!()),
"{_err}"
); } }
);
return #Outcome::Forward((#__data, #Status::UnprocessableEntity));
@ -128,17 +132,27 @@ fn request_guard_decl(guard: &Guard) -> TokenStream {
let #ident: #ty = match <#ty as #FromRequest>::from_request(#__req).await {
#Outcome::Success(__v) => __v,
#Outcome::Forward(__e) => {
::rocket::trace::info!(name: "forward", parameter = stringify!(#ident),
type_name = stringify!(#ty), status = __e.code,
"request guard forwarding");
::rocket::trace::info!(
name: "forward",
target: concat!("rocket::codegen::route::", module_path!()),
parameter = stringify!(#ident),
type_name = stringify!(#ty),
status = __e.code,
"request guard forwarding"
);
return #Outcome::Forward((#__data, __e));
},
#[allow(unreachable_code)]
#Outcome::Error((__c, __e)) => {
::rocket::trace::info!(name: "failure", parameter = stringify!(#ident),
type_name = stringify!(#ty), reason = %#display_hack!(__e),
"request guard failed");
::rocket::trace::info!(
name: "failure",
target: concat!("rocket::codegen::route::", module_path!()),
parameter = stringify!(#ident),
type_name = stringify!(#ty),
reason = %#display_hack!(__e),
"request guard failed"
);
return #Outcome::Error(__c);
}
@ -155,9 +169,14 @@ fn param_guard_decl(guard: &Guard) -> TokenStream {
// Returned when a dynamic parameter fails to parse.
let parse_error = quote!({
::rocket::trace::info!(name: "forward", parameter = #name,
type_name = stringify!(#ty), reason = %#display_hack!(__error),
"path guard forwarding");
::rocket::trace::info!(
name: "forward",
target: concat!("rocket::codegen::route::", module_path!()),
parameter = #name,
type_name = stringify!(#ty),
reason = %#display_hack!(__error),
"path guard forwarding"
);
#Outcome::Forward((#__data, #Status::UnprocessableEntity))
});
@ -174,9 +193,12 @@ fn param_guard_decl(guard: &Guard) -> TokenStream {
},
#_None => {
::rocket::trace::error!(
target: concat!("rocket::codegen::route::", module_path!()),
"Internal invariant broken: dyn param {} not found.\n\
Please report this to the Rocket issue tracker.\n\
https://github.com/rwf2/Rocket/issues", #i);
https://github.com/rwf2/Rocket/issues",
#i
);
return #Outcome::Forward((#__data, #Status::InternalServerError));
}
@ -203,17 +225,27 @@ fn data_guard_decl(guard: &Guard) -> TokenStream {
let #ident: #ty = match <#ty as #FromData>::from_data(#__req, #__data).await {
#Outcome::Success(__d) => __d,
#Outcome::Forward((__d, __e)) => {
::rocket::trace::info!(name: "forward", parameter = stringify!(#ident),
type_name = stringify!(#ty), status = __e.code,
"data guard forwarding");
::rocket::trace::info!(
name: "forward",
target: concat!("rocket::codegen::route::", module_path!()),
parameter = stringify!(#ident),
type_name = stringify!(#ty),
status = __e.code,
"data guard forwarding"
);
return #Outcome::Forward((__d, __e));
}
#[allow(unreachable_code)]
#Outcome::Error((__c, __e)) => {
::rocket::trace::info!(name: "failure", parameter = stringify!(#ident),
type_name = stringify!(#ty), reason = %#display_hack!(__e),
"data guard failed");
::rocket::trace::info!(
name: "failure",
target: concat!("rocket::codegen::route::", module_path!()),
parameter = stringify!(#ident),
type_name = stringify!(#ty),
reason = %#display_hack!(__e),
"data guard failed"
);
return #Outcome::Error(__c);
}

View File

@ -178,8 +178,9 @@ impl<'a> Segments<'a, Path> {
}
/// Creates a `PathBuf` from `self`. The returned `PathBuf` is
/// percent-decoded. If a segment is equal to `..`, the previous segment (if
/// any) is skipped.
/// percent-decoded and guaranteed to be relative. If a segment is equal to
/// `.`, 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
/// 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
/// 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`
/// is safe to interpolate within, or use as a suffix of, a path without
@ -216,7 +217,9 @@ impl<'a> Segments<'a, Path> {
pub fn to_path_buf(&self, allow_dotfiles: bool) -> Result<PathBuf, PathError> {
let mut buf = PathBuf::new();
for segment in self.clone() {
if segment == ".." {
if segment == "." {
continue;
} else if segment == ".." {
buf.pop();
} else if !allow_dotfiles && segment.starts_with('.') {
return Err(PathError::BadStart('.'))

View File

@ -5,8 +5,59 @@ mod named_file;
mod temp_file;
mod file_name;
pub mod rewrite;
pub use server::*;
pub use named_file::*;
pub use temp_file::*;
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)
}
};
}
}

261
core/lib/src/fs/rewrite.rs Normal file
View File

@ -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()))
}
}

View File

@ -1,32 +1,21 @@
use std::path::{PathBuf, Path};
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::borrow::Cow;
use crate::{Request, Data};
use crate::http::{Method, Status, uri::Segments, ext::IntoOwned};
use crate::route::{Route, Handler, Outcome};
use crate::response::{Redirect, Responder};
use crate::{response, Data, Request, Response};
use crate::outcome::IntoOutcome;
use crate::fs::NamedFile;
use crate::http::{uri::Segments, HeaderMap, Method, ContentType, Status};
use crate::route::{Route, Handler, Outcome};
use crate::response::Responder;
use crate::util::Formatter;
use crate::fs::rewrite::*;
/// Custom handler for serving static files.
///
/// This handler makes it simple to serve static files from a directory on the
/// local file system. To use it, construct a `FileServer` using either
/// [`FileServer::from()`] or [`FileServer::new()`] then simply `mount` the
/// handler at a desired path. When mounted, the handler will generate route(s)
/// that serve the desired static files. If a requested file is not found, the
/// routes _forward_ the incoming request. The default rank of the generated
/// routes is `10`. To customize route ranking, use the [`FileServer::rank()`]
/// method.
///
/// # Options
///
/// The handler's functionality can be customized by passing an [`Options`] to
/// [`FileServer::new()`].
///
/// # Example
///
/// Serve files from the `/static` directory on the local file system at the
/// `/public` path with the [default options](#impl-Default):
/// This handler makes is simple to serve static files from a directory on the
/// local file system. To use it, construct a `FileServer` using
/// [`FileServer::new()`], then `mount` the handler.
///
/// ```rust,no_run
/// # #[macro_use] extern crate rocket;
@ -34,14 +23,44 @@ use crate::fs::NamedFile;
///
/// #[launch]
/// 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"))
/// }
/// ```
///
/// Requests for files at `/public/<path..>` will be handled by returning the
/// contents of `/static/<path..>`. Requests for _directories_ at
/// `/public/<directory>` will be handled by returning the contents of
/// `/static/<directory>/index.html`.
/// contents of `/static/<path..>`. Requests for directories will return the
/// contents of `index.html`.
///
/// ## Relative Paths
///
@ -57,13 +76,14 @@ use crate::fs::NamedFile;
///
/// #[launch]
/// fn rocket() -> _ {
/// rocket::build().mount("/", FileServer::from(relative!("static")))
/// rocket::build().mount("/", FileServer::new(relative!("static")))
/// }
/// ```
#[derive(Debug, Clone)]
///
/// [`relative!`]: crate::fs::relative!
#[derive(Clone)]
pub struct FileServer {
root: PathBuf,
options: Options,
rewrites: Vec<Arc<dyn Rewriter>>,
rank: isize,
}
@ -72,120 +92,243 @@ impl FileServer {
const DEFAULT_RANK: isize = 10;
/// Constructs a new `FileServer` that serves files from the file system
/// `path`. By default, [`Options::Index`] is set, and the generated routes
/// 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()`].
/// `path` with the following rewrites:
///
/// # Panics
/// - `|f, _| f.is_visible()`: Serve only visible files (hide dotfiles).
/// - [`Prefix::checked(path)`]: Prefix requests with `path`.
/// - [`TrailingDirs`]: Ensure directory have a trailing slash.
/// - [`DirIndex::unconditional("index.html")`]: Serve `$dir/index.html` for
/// requests to directory `$dir`.
///
/// Panics if `path` does not exist or is not a directory.
/// 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
///
/// Serve the static files in the `/www/public` local directory on path
/// `/static`.
///
/// ```rust,no_run
/// # #[macro_use] extern crate rocket;
/// use rocket::fs::FileServer;
///
/// #[launch]
/// fn rocket() -> _ {
/// rocket::build().mount("/static", FileServer::from("/www/public"))
/// }
/// ```
///
/// Exactly as before, but set the rank for generated routes to `30`.
///
/// ```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 {
FileServer::new(path, Options::default())
}
/// Constructs a new `FileServer` that serves files from the file system
/// `path` with `options` enabled. By default, the handler's routes have a
/// rank of `10`. To choose a different rank, use [`FileServer::rank()`].
///
/// # Panics
///
/// If [`Options::Missing`] is not set, panics if `path` does not exist or
/// is not a directory. Otherwise does not panic.
///
/// # Example
///
/// Serve the static files in the `/www/public` local directory on path
/// `/static` without serving index files or dot files. Additionally, serve
/// the same files on `/pub` with a route rank of -1 while also serving
/// index files and dot files.
///
/// ```rust,no_run
/// # #[macro_use] extern crate rocket;
/// use rocket::fs::{FileServer, Options};
///
/// #[launch]
/// fn rocket() -> _ {
/// let options = Options::Index | Options::DotFiles;
/// rocket::build()
/// .mount("/static", FileServer::from("/www/public"))
/// .mount("/pub", FileServer::new("/www/public", options).rank(-1))
/// .mount("/", FileServer::new("/www/static"))
/// }
/// ```
#[track_caller]
pub fn new<P: AsRef<Path>>(path: P, options: Options) -> Self {
let path = path.as_ref();
if !options.contains(Options::Missing) {
if !options.contains(Options::IndexFile) && !path.is_dir() {
error!(path = %path.display(),
"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 }
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"))
}
/// Sets the rank for generated routes to `rank`.
/// 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
/// use rocket::fs::{FileServer, Options};
/// # #[macro_use] extern crate rocket;
/// use rocket::fs::{FileServer, rewrite};
///
/// // A `FileServer` created with `from()` with routes of rank `3`.
/// FileServer::from("/public").rank(3);
/// #[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"));
///
/// // A `FileServer` created with `new()` with routes of rank `-15`.
/// FileServer::new("/public", Options::Index).rank(-15);
/// rocket::build()
/// .mount("/", server)
/// }
/// ```
pub fn identity() -> Self {
Self {
rewrites: vec![],
rank: Self::DEFAULT_RANK
}
}
/// Sets the rank of the route emitted by the `FileServer` to `rank`.
///
/// # Example
///
/// ```rust,no_run
/// # use rocket::fs::FileServer;
/// # fn make_server() -> FileServer {
/// FileServer::identity()
/// .rank(5)
/// # }
pub fn rank(mut self, rank: isize) -> Self {
self.rank = rank;
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()))
/// }
///
/// #[launch]
/// fn rocket() -> _ {
/// rocket::build()
/// .mount("/", FileServer::new("static").rewrite(redir_missing))
/// }
/// ```
///
/// 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.
pub fn rewrite<R: Rewriter>(mut self, rewriter: R) -> Self {
self.rewrites.push(Arc::new(rewriter));
self
}
/// Adds a rewriter to the pipeline that returns `Some` only when the
/// function `f` returns `true`, filtering out all other files.
///
/// # Example
///
/// Allow all files that don't have a file name or have a file name other
/// than "hidden".
///
/// ```rust,no_run
/// # #[macro_use] extern crate rocket;
/// use rocket::fs::FileServer;
///
/// #[launch]
/// fn rocket() -> _ {
/// let server = FileServer::new("static")
/// .filter(|f, _| f.path.file_name() != Some("hidden".as_ref()));
///
/// rocket::build()
/// .mount("/", server)
/// }
/// ```
pub fn filter<F: Send + Sync + 'static>(self, f: F) -> Self
where F: Fn(&File<'_>, &Request<'_>) -> bool
{
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))
}
/// 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
///
/// Append `index.txt` to every path.
///
/// ```rust,no_run
/// # #[macro_use] extern crate rocket;
/// use rocket::fs::FileServer;
///
/// #[launch]
/// fn rocket() -> _ {
/// let server = FileServer::new("static")
/// .map(|f, _| f.map_path(|p| p.join("index.txt")).into());
///
/// rocket::build()
/// .mount("/", server)
/// }
/// ```
pub fn map<F: Send + Sync + 'static>(self, f: F) -> Self
where F: for<'r> Fn(File<'r>, &Request<'_>) -> Rewrite<'r>
{
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> {
fn from(server: FileServer) -> Self {
let source = figment::Source::File(server.root.clone());
let mut route = Route::ranked(server.rank, Method::Get, "/<path..>", server);
route.name = Some(format!("FileServer: {}", source).into());
route.name = Some("FileServer".into());
vec![route]
}
}
@ -193,267 +336,71 @@ impl From<FileServer> for Vec<Route> {
#[crate::async_trait]
impl Handler for FileServer {
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());
// TODO: Should we reject dotfiles for `self.root` if !DotFiles?
let options = self.options;
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);
}
let mut response = path.map(|p| Rewrite::File(File::new(p)));
for rewrite in &self.rewrites {
response = rewrite.rewrite(response, req);
}
// Get the segments as a `PathBuf`, allowing dotfiles requested.
let allow_dotfiles = options.contains(Options::DotFiles);
let path = req.segments::<Segments<'_, Path>>(0..).ok()
.and_then(|segments| segments.to_path_buf(allow_dotfiles).ok())
.map(|path| self.root.join(path));
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),
}
}
}
/// 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! {
/// 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::from(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)
}
let (outcome, status) = match response {
Some(Rewrite::File(f)) => (f.open().await.respond_to(req), Status::NotFound),
Some(Rewrite::Redirect(r)) => (r.respond_to(req), Status::InternalServerError),
None => return Outcome::forward(data, Status::NotFound),
};
outcome.or_forward((data, status))
}
}
impl fmt::Debug for FileServer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FileServer")
.field("rewrites", &Formatter(|f| write!(f, "<{} rewrites>", self.rewrites.len())))
.field("rank", &self.rank)
.finish()
}
}
impl<'r> File<'r> {
async fn open(self) -> std::io::Result<NamedFile<'r>> {
let file = tokio::fs::File::open(&self.path).await?;
let metadata = file.metadata().await?;
if metadata.is_dir() {
return Err(std::io::Error::other("is a directory"));
}
Ok(NamedFile {
file,
len: metadata.len(),
path: self.path,
headers: self.headers,
})
}
}
struct NamedFile<'r> {
file: tokio::fs::File,
len: u64,
path: Cow<'r, Path>,
headers: HeaderMap<'r>,
}
// Do we want to allow the user to rewrite the Content-Type?
impl<'r> Responder<'r, 'r> for NamedFile<'r> {
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'r> {
let mut response = Response::new();
response.set_header_map(self.headers);
if !response.headers().contains("Content-Type") {
self.path.extension()
.and_then(|ext| ext.to_str())
.and_then(ContentType::from_extension)
.map(|content_type| response.set_header(content_type));
}
response.set_sized_body(self.len as usize, self.file);
Ok(response)
}
}

View File

@ -45,7 +45,7 @@ use crate::http::Status;
///
/// [`Origin`]: crate::http::uri::Origin
/// [`uri!`]: ../macro.uri.html
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Redirect(Status, Option<Reference<'static>>);
impl Redirect {
@ -144,6 +144,12 @@ impl Redirect {
pub fn moved<U: TryInto<Reference<'static>>>(uri: U) -> Redirect {
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

View File

@ -637,6 +637,10 @@ impl<'r> Response<'r> {
&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
/// `header.name` will be lost, and only `header` will remain. The type of
/// `header` can be any type that implements `Into<Header>`. See [trait

View File

@ -39,10 +39,10 @@ impl RocketDynFmt {
return;
}
let workers = config.map(|c| c.workers).unwrap_or(num_cpus::get());
let colors = config.map(|c| c.cli_colors).unwrap_or(CliColors::Auto);
let level = config.map(|c| c.log_level).unwrap_or(Some(Level::INFO));
let format = config.map(|c| c.log_format).unwrap_or(TraceFormat::Pretty);
let workers = config.map_or(num_cpus::get(), |c| c.workers);
let colors = config.map_or(CliColors::Auto, |c| c.cli_colors);
let level = config.map_or(Some(Level::INFO), |c| c.log_level);
let format = config.map_or(TraceFormat::Pretty, |c| c.log_format);
let formatter = |format| match format {
TraceFormat::Pretty => Self::from(RocketFmt::<Pretty>::new(workers, colors, level)),
@ -57,7 +57,7 @@ impl RocketDynFmt {
if result.is_ok() {
assert!(HANDLE.set(reload_handle).is_ok());
} if let Some(handle) = HANDLE.get() {
} else if let Some(handle) = HANDLE.get() {
assert!(handle.modify(|layer| *layer = formatter(format)).is_ok());
}
}

View File

@ -1,10 +1,10 @@
use std::{io::Read, fs::File};
use std::{io::Read, fs};
use std::path::Path;
use rocket::{Rocket, Route, Build};
use rocket::http::Status;
use rocket::local::blocking::Client;
use rocket::fs::{FileServer, Options, relative};
use rocket::fs::{FileServer, relative, rewrite::*};
fn static_root() -> &'static Path {
Path::new(relative!("/tests/static"))
@ -13,13 +13,66 @@ fn static_root() -> &'static Path {
fn rocket() -> Rocket<Build> {
let root = static_root();
rocket::build()
.mount("/default", FileServer::from(&root))
.mount("/no_index", FileServer::new(&root, Options::None))
.mount("/dots", FileServer::new(&root, Options::DotFiles))
.mount("/index", FileServer::new(&root, Options::Index))
.mount("/both", FileServer::new(&root, Options::DotFiles | Options::Index))
.mount("/redir", FileServer::new(&root, Options::NormalizeDirs))
.mount("/redir_index", FileServer::new(&root, Options::NormalizeDirs | Options::Index))
.mount("/default", FileServer::new(&root))
.mount(
"/no_index",
FileServer::identity()
.filter(|f, _| f.is_visible())
.rewrite(Prefix::checked(&root))
)
.mount(
"/dots",
FileServer::identity()
.rewrite(Prefix::checked(&root))
)
.mount(
"/index",
FileServer::identity()
.filter(|f, _| f.is_visible())
.rewrite(Prefix::checked(&root))
.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(
"/both",
FileServer::identity()
.rewrite(Prefix::checked(&root))
.rewrite(DirIndex::unconditional("index.html"))
)
.mount(
"/redir",
FileServer::identity()
.filter(|f, _| f.is_visible())
.rewrite(Prefix::checked(&root))
.rewrite(TrailingDirs)
)
.mount(
"/redir_index",
FileServer::identity()
.filter(|f, _| f.is_visible())
.rewrite(Prefix::checked(&root))
.rewrite(TrailingDirs)
.rewrite(DirIndex::unconditional("index.html"))
)
.mount(
"/index_file",
FileServer::identity()
.filter(|f, _| f.is_visible())
.rewrite(File::checked(root.join("other/hello.txt")))
)
.mount(
"/missing_root",
FileServer::identity()
.filter(|f, _| f.is_visible())
.rewrite(File::new(root.join("no_file")))
)
}
static REGULAR_FILES: &[&str] = &[
@ -27,6 +80,7 @@ static REGULAR_FILES: &[&str] = &[
"inner/goodbye",
"inner/index.html",
"other/hello.txt",
"other/index.htm",
];
static HIDDEN_FILES: &[&str] = &[
@ -39,18 +93,18 @@ static INDEXED_DIRECTORIES: &[&str] = &[
"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 response = client.get(full_path).dispatch();
if exists {
if let Some(disk_path) = disk_path {
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() {
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();
file.read_to_string(&mut expected_contents).expect("read file");
assert_eq!(response.into_string(), Some(expected_contents));
@ -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) {
for path in paths.iter() {
assert_file(client, prefix, path, exist);
@ -101,12 +163,35 @@ fn test_static_all() {
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]
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]
fn test_ranking() {
let root = static_root();
for rank in -128..128 {
let a = FileServer::new(&root, Options::None).rank(rank);
let b = FileServer::from(&root).rank(rank);
let a = FileServer::new(&root).rank(rank);
let b = FileServer::new(&root).rank(rank);
for handler in vec![a, b] {
let routes: Vec<Route> = handler.into();
@ -153,15 +238,15 @@ fn test_redirection() {
assert_eq!(response.status(), Status::Ok);
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/"));
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"));
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/"));
// Paths with trailing slash are unaffected.
@ -179,14 +264,32 @@ fn test_redirection() {
assert_eq!(response.status(), Status::Ok);
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/"));
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/"));
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/"));
}
#[test]
#[should_panic]
fn test_panic_on_missing_file() {
let _ = File::checked(static_root().join("missing_file"));
}
#[test]
#[should_panic]
fn test_panic_on_missing_dir() {
let _ = Prefix::checked(static_root().join("missing_dir"));
}
#[test]
#[should_panic]
fn test_panic_on_file_not_dir() {
let _ = Prefix::checked(static_root().join("index.html"));
}

View File

@ -1 +1 @@
Inner index.
Inner index.html

View File

@ -0,0 +1 @@
Inner index.htm

View File

@ -169,7 +169,7 @@ async fn files(file: PathBuf) -> Option<NamedFile> {
fn rocket() -> _ {
rocket::build()
// serve files from `/www/static` at path `/public`
.mount("/public", FileServer::from("/www/static"))
.mount("/public", FileServer::new("/www/static"))
}
```

View File

@ -354,7 +354,7 @@ are:
* [`Flash`] - Sets a "flash" cookie that is removed when accessed.
* [`Json`] - Automatically serializes values into JSON.
* [`MsgPack`] - Automatically serializes values into MessagePack.
* [`Template`] - Renders a dynamic template using handlebars or Tera.
* [`Template`] - Renders a dynamic template using Handlebars, Tera or MiniJinja.
[`status`]: @api/master/rocket/response/status/
[`content`]: @api/master/rocket/response/content/
@ -589,7 +589,7 @@ reloading is disabled.
The [`Template`] API documentation contains more information about templates,
including how to customize a template engine to add custom helpers and filters.
The [templating example](@git/master/examples/templating) uses both Tera and Handlebars
The [templating example](@git/master/examples/templating) uses Tera, Handlebars and MiniJinja
templating to implement the same application.
[configurable]: ../configuration/

View File

@ -49,7 +49,7 @@ For any deployment, it's important to keep in mind:
#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/", FileServer::from("./static"))
.mount("/", FileServer::new("./static"))
.attach(Template::fairing())
}
```

View File

@ -54,5 +54,5 @@ fn rocket() -> _ {
rocket::build()
.manage(channel::<Message>(1024).0)
.mount("/", routes![post, events])
.mount("/", FileServer::from(relative!("static")))
.mount("/", FileServer::new(relative!("static")))
}

View File

@ -93,5 +93,5 @@ fn rocket() -> _ {
rocket::build()
.mount("/", routes![index, submit])
.attach(Template::fairing())
.mount("/", FileServer::from(relative!("/static")))
.mount("/", FileServer::new(relative!("/static")))
}

View File

@ -23,5 +23,5 @@ mod manual {
fn rocket() -> _ {
rocket::build()
.mount("/", rocket::routes![manual::second])
.mount("/", FileServer::from(relative!("static")))
.mount("/", FileServer::new(relative!("static")))
}

View File

@ -63,7 +63,7 @@ fn test_icon_file() {
#[test]
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?a=b&c=d", None, Status::NotFound);

View File

@ -11,4 +11,4 @@ rocket = { path = "../../core/lib" }
# in your application, you should enable only the template engine(s) used
[dependencies.rocket_dyn_templates]
path = "../../contrib/dyn_templates"
features = ["tera", "handlebars"]
features = ["tera", "handlebars", "minijinja"]

View File

@ -1,16 +1,23 @@
#[macro_use] extern crate rocket;
#[macro_use]
extern crate rocket;
mod hbs;
mod minijinja;
mod tera;
#[cfg(test)] mod tests;
#[cfg(test)]
mod tests;
use rocket::response::content::RawHtml;
use rocket_dyn_templates::Template;
#[get("/")]
fn index() -> RawHtml<&'static str> {
RawHtml(r#"See <a href="tera">Tera</a> or <a href="hbs">Handlebars</a>."#)
RawHtml(
r#"See <a href="tera">Tera</a>,
<a href="hbs">Handlebars</a>,
or <a href="minijinja">MiniJinja</a>."#,
)
}
#[launch]
@ -19,10 +26,16 @@ fn rocket() -> _ {
.mount("/", routes![index])
.mount("/tera", routes![tera::index, tera::hello, tera::about])
.mount("/hbs", routes![hbs::index, hbs::hello, hbs::about])
.mount(
"/minijinja",
routes![minijinja::index, minijinja::hello, minijinja::about],
)
.register("/hbs", catchers![hbs::not_found])
.register("/tera", catchers![tera::not_found])
.register("/minijinja", catchers![minijinja::not_found])
.attach(Template::custom(|engines| {
hbs::customize(&mut engines.handlebars);
tera::customize(&mut engines.tera);
minijinja::customize(&mut engines.minijinja);
}))
}

View File

@ -0,0 +1,61 @@
use rocket::response::Redirect;
use rocket::Request;
use rocket_dyn_templates::{context, minijinja::Environment, Template};
// use self::minijinja::;
#[get("/")]
pub fn index() -> Redirect {
Redirect::to(uri!("/minijinja", hello(name = "Your Name")))
}
#[get("/hello/<name>")]
pub fn hello(name: &str) -> Template {
Template::render(
"minijinja/index",
context! {
title: "Hello",
name: Some(name),
items: vec!["One", "Two", "Three"],
},
)
}
#[get("/about")]
pub fn about() -> Template {
Template::render(
"minijinja/about.html",
context! {
title: "About",
},
)
}
#[catch(404)]
pub fn not_found(req: &Request<'_>) -> Template {
println!("Handling 404 for URI: {}", req.uri());
Template::render(
"minijinja/error/404",
context! {
uri: req.uri()
},
)
}
pub fn customize(env: &mut Environment) {
env.add_template(
"minijinja/about.html",
r#"
{% extends "minijinja/layout" %}
{% block page %}
<section id="about">
<h1>About - Here's another page!</h1>
</section>
{% endblock %}
"#,
)
.expect("valid Jinja2 template");
}

View File

@ -40,11 +40,11 @@ fn test_404(base: &str) {
let client = Client::tracked(rocket()).unwrap();
for bad_path in &["/hello", "/foo/bar", "/404"] {
let path = format!("/{}{}", base, bad_path);
let escaped_path = RawStr::new(&path).html_escape();
let escaped_path = RawStr::new(&path).html_escape().to_lowercase();
let response = client.get(&path).dispatch();
assert_eq!(response.status(), Status::NotFound);
let response = response.into_string().unwrap();
let response = response.into_string().unwrap().to_lowercase();
assert!(response.contains(base));
assert! {
@ -66,6 +66,7 @@ fn test_index() {
let response = client.get("/").dispatch().into_string().unwrap();
assert!(response.contains("Tera"));
assert!(response.contains("Handlebars"));
assert!(response.contains("MiniJinja"));
}
#[test]
@ -83,3 +84,11 @@ fn tera() {
test_404("tera");
test_about("tera");
}
#[test]
fn minijinja() {
test_root("minijinja");
test_name("minijinja");
test_404("minijinja");
test_about("minijinja");
}

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>404 - minijinja</title>
</head>
<body>
<h1>404: Hey! There's nothing here.</h1>
The page at {{ uri }} does not exist!
</body>
</html>

View File

@ -0,0 +1,3 @@
<footer>
<a href="/">Home</a>
</footer>

View File

@ -0,0 +1,17 @@
{% extends "minijinja/layout" %}
{% block page %}
<section id="hello">
<h1>Hi {{ name }}!</h1>
<h3>Here are your items:</h3>
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
</section>
<section id="custom-helper">
<p>Try going to <a href="/minijinja/hello/Your%20Name">/minijinja/hello/Your Name</a>.</p>
</section>
{% endblock %}

View File

@ -0,0 +1,11 @@
<!doctype html>
<html>
<head>
<title>Rocket Example - {{ title }}</title>
</head>
<body>
{% include "minijinja/nav" %}
{% block page %}{% endblock %}
{% include "minijinja/footer" %}
</body>
</html>

View File

@ -0,0 +1 @@
<a href="/minijinja/hello/Unknown">Hello</a> | <a href="/minijinja/about">About</a>

View File

@ -110,7 +110,7 @@ fn rocket() -> _ {
.attach(DbConn::fairing())
.attach(Template::fairing())
.attach(AdHoc::on_ignite("Run Migrations", run_migrations))
.mount("/", FileServer::from(relative!("static")))
.mount("/", FileServer::new(relative!("static")))
.mount("/", routes![index])
.mount("/todo", routes![new, toggle, delete])
}

View File

@ -39,5 +39,5 @@ fn echo_raw(ws: ws::WebSocket) -> ws::Stream!['static] {
fn rocket() -> _ {
rocket::build()
.mount("/", routes![echo_channel, echo_stream, echo_raw])
.mount("/", FileServer::from(fs::relative!("static")))
.mount("/", FileServer::new(fs::relative!("static")))
}