From f50b6043e8b0c78ecf5b6f160190120887549e07 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sat, 29 Jun 2024 21:22:59 -0500 Subject: [PATCH] Improve FileServer rewrite API. Finalizes the FileServer rewrite API implementation. Primarily reworks how the built-in rewriters are written (now as structs instead of free functions) and reorganizes the `fs` module. Co-authored-by: Matthew Pomes --- core/http/src/uri/segments.rs | 13 +- core/lib/src/fs/mod.rs | 53 +- core/lib/src/fs/rewrite.rs | 261 ++++++++ core/lib/src/fs/server.rs | 838 +++++++++---------------- core/lib/src/response/redirect.rs | 2 +- core/lib/src/response/response.rs | 4 + core/lib/tests/file_server.rs | 109 ++-- core/lib/tests/static/inner/index.html | 2 +- core/lib/tests/static/other/index.htm | 1 + docs/guide/05-requests.md | 2 +- docs/guide/11-deploying.md | 2 +- examples/chat/src/main.rs | 2 +- examples/forms/src/main.rs | 2 +- examples/static-files/src/main.rs | 2 +- examples/static-files/src/tests.rs | 2 +- examples/todo/src/main.rs | 2 +- examples/upgrade/src/main.rs | 2 +- 17 files changed, 686 insertions(+), 613 deletions(-) create mode 100644 core/lib/src/fs/rewrite.rs create mode 100644 core/lib/tests/static/other/index.htm diff --git a/core/http/src/uri/segments.rs b/core/http/src/uri/segments.rs index 98f5184b..c6c6b52f 100644 --- a/core/http/src/uri/segments.rs +++ b/core/http/src/uri/segments.rs @@ -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,10 +217,10 @@ impl<'a> Segments<'a, Path> { pub fn to_path_buf(&self, allow_dotfiles: bool) -> Result { let mut buf = PathBuf::new(); for segment in self.clone() { - if segment == ".." { - buf.pop(); - } else if segment == "." { + if segment == "." { continue; + } else if segment == ".." { + buf.pop(); } else if !allow_dotfiles && segment.starts_with('.') { return Err(PathError::BadStart('.')) } else if segment.starts_with('*') { diff --git a/core/lib/src/fs/mod.rs b/core/lib/src/fs/mod.rs index dd00f179..94a0145a 100644 --- a/core/lib/src/fs/mod.rs +++ b/core/lib/src/fs/mod.rs @@ -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) + } + }; + } +} diff --git a/core/lib/src/fs/rewrite.rs b/core/lib/src/fs/rewrite.rs new file mode 100644 index 00000000..00b57357 --- /dev/null +++ b/core/lib/src/fs/rewrite.rs @@ -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>, req: &'r Request<'_>) -> Option>; +} + +/// 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>) -> 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>(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(self, f: F) -> Self + where F: FnOnce(Cow<'r, Path>) -> P, P: Into>, + { + 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>(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>(path: P) -> Self { + Self(path.as_ref().to_path_buf()) + } +} + +impl Rewriter for Prefix { + fn rewrite<'r>(&self, opt: Option>, _: &Request<'_>) -> Option> { + 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>, _: &Request<'_>) -> Option> { + 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>, req: &Request<'_>) -> Option> { + 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) -> 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) -> Self { + Self { path: path.as_ref().to_path_buf(), check: true } + } +} + +impl Rewriter for DirIndex { + fn rewrite<'r>(&self, opt: Option>, _: &Request<'_>) -> Option> { + 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> for Rewrite<'r> { + fn from(value: File<'r>) -> Self { + Self::File(value) + } +} + +impl<'r> From for Rewrite<'r> { + fn from(value: Redirect) -> Self { + Self::Redirect(value) + } +} + +impl Rewriter for F + where F: for<'r> Fn(Option>, &Request<'_>) -> Option> +{ + fn rewrite<'r>(&self, f: Option>, r: &Request<'_>) -> Option> { + self(f, r) + } +} + +impl Rewriter for Rewrite<'static> { + fn rewrite<'r>(&self, _: Option>, _: &Request<'_>) -> Option> { + Some(self.clone()) + } +} + +impl Rewriter for File<'static> { + fn rewrite<'r>(&self, _: Option>, _: &Request<'_>) -> Option> { + Some(Rewrite::File(self.clone())) + } +} + +impl Rewriter for Redirect { + fn rewrite<'r>(&self, _: Option>, _: &Request<'_>) -> Option> { + Some(Rewrite::Redirect(self.clone())) + } +} diff --git a/core/lib/src/fs/server.rs b/core/lib/src/fs/server.rs index d99999b8..2f6efe7a 100644 --- a/core/lib/src/fs/server.rs +++ b/core/lib/src/fs/server.rs @@ -1,46 +1,21 @@ -use core::fmt; -use std::borrow::Cow; -use std::ffi::OsStr; -use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR}; +use std::fmt; +use std::path::{Path, PathBuf}; use std::sync::Arc; +use std::borrow::Cow; -use crate::fs::NamedFile; -use crate::{Data, Request, outcome::IntoOutcome}; -use crate::http::{ - Method, - HeaderMap, - Header, - uri::Segments, - Status, - ext::IntoOwned, -}; +use crate::{response, Data, Request, Response}; +use crate::outcome::IntoOutcome; +use crate::http::{uri::Segments, HeaderMap, Method, ContentType, Status}; use crate::route::{Route, Handler, Outcome}; -use crate::response::{Redirect, Responder}; +use crate::response::Responder; +use crate::util::Formatter; +use crate::fs::rewrite::*; /// Custom handler for serving static files. /// /// 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::from()`], then simply `mount` the handler. When mounted, the -/// handler serves files from the specified directory. If the file is not found, -/// the handler _forwards_ the request. By default, `FileServer` has a rank of -/// `10`. Use [`FileServer::new()`] to create a route with a custom rank. -/// -/// # Customization -/// -/// How `FileServer` responds to specific requests can be customized, through -/// the use of [`Rewriter`]s. See [`Rewriter`] for more detailed documentation -/// on how to take full advantage of the customization of `FileServer`. -/// -/// [`FileServer::from()`] and [`FileServer::new()`] automatically add some common -/// rewrites. They filter out dotfiles, redirect folder accesses to include a trailing -/// slash, and use `index.html` to respond to requests for a directory. If you want -/// to customize or replace these default rewrites, see [`FileServer::empty()`]. -/// -/// # Example -/// -/// Serve files from the `/static` directory on the local file system at the -/// `/public` path, with the default rewrites. +/// [`FileServer::new()`], then `mount` the handler. /// /// ```rust,no_run /// # #[macro_use] extern crate rocket; @@ -48,7 +23,38 @@ use crate::response::{Redirect, Responder}; /// /// #[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")) /// } /// ``` /// @@ -70,589 +76,331 @@ use crate::response::{Redirect, Responder}; /// /// #[launch] /// fn rocket() -> _ { -/// rocket::build().mount("/", FileServer::from(relative!("static"))) +/// rocket::build().mount("/", FileServer::new(relative!("static"))) /// } /// ``` +/// +/// [`relative!`]: crate::fs::relative! #[derive(Clone)] pub struct FileServer { rewrites: Vec>, 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>); - -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) -/// 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>, req: &Request<'_>) - -> Option>; -} - -/// 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> for FileResponse<'p, 'h> { - fn from(value: File<'p, 'h>) -> Self { - Self::File(value) - } -} -impl<'p, 'h> From> for Option> { - fn from(value: File<'p, 'h>) -> Self { - Some(FileResponse::File(value)) - } -} - -impl<'p, 'h> From for FileResponse<'p, 'h> { - fn from(value: Redirect) -> Self { - Self::Redirect(value) - } -} -impl<'p, 'h> From for Option> { - 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>>(mut self, header: H) -> Self { - self.headers.add(header); - self - } - - /// Replace the path of this `File`. - pub fn with_path(self, path: impl Into>) -> Self { - Self { - path: path.into(), - headers: self.headers, - } - } - - /// Replace the path of this `File`. - pub fn map_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 Rewriter for F - where F: for<'r, 'h> Fn(Option>, &Request<'_>) - -> Option> -{ - fn rewrite<'p, 'h>(&self, path: Option>, req: &Request<'_>) - -> Option> - { - self(path, req) - } -} - -/// Helper to implement [`FileServer::filter_file()`] -struct FilterFile(F); -impl Rewriter for FilterFile - where F: Fn(&File<'_, '_>, &Request<'_>) -> bool + Send + Sync + 'static -{ - fn rewrite<'p, 'h>(&self, path: Option>, req: &Request<'_>) - -> Option> - { - match path { - Some(FileResponse::File(file)) if !self.0(&file, req) => None, - path => path, - } - } -} - -/// Helper to implement [`FileServer::map_file()`] -struct MapFile(F); -impl Rewriter for MapFile - where F: for<'p, 'h> Fn(File<'p, 'h>, &Request<'_>) - -> FileResponse<'p, 'h> + Send + Sync + 'static, -{ - fn rewrite<'p, 'h>(&self, path: Option>, req: &Request<'_>) - -> Option> - { - 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 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 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) -> 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) -> 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) -> 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 { /// The default rank use by `FileServer` routes. const DEFAULT_RANK: isize = 10; - /// Constructs a new `FileServer`, with default rank, and no - /// rewrites. + /// Constructs a new `FileServer` that serves files from the file system + /// `path` with the following rewrites: /// - /// See [`FileServer::empty_ranked()`]. - pub fn empty() -> Self { - Self::empty_ranked(Self::DEFAULT_RANK) - } - - /// Constructs a new `FileServer`, with specified rank, and no - /// rewrites. + /// - `|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`. + /// + /// 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 /// - /// Replicate the output of [`FileServer::new()`]. /// ```rust,no_run - /// # use rocket::fs::{FileServer, filter_dotfiles, dir_root, normalize_dirs}; - /// # fn launch() -> FileServer { - /// FileServer::empty_ranked(10) - /// .filter_file(filter_dotfiles) - /// .map_file(dir_root("static")) - /// .map_file(normalize_dirs) - /// # } + /// # #[macro_use] extern crate rocket; + /// use rocket::fs::FileServer; + /// + /// #[launch] + /// fn rocket() -> _ { + /// rocket::build() + /// .mount("/", FileServer::new("/www/static")) + /// } /// ``` - pub fn empty_ranked(rank: isize) -> Self { + pub fn new>(path: P) -> Self { + Self::identity() + .filter(|f, _| f.is_visible()) + .rewrite(Prefix::checked(path)) + .rewrite(TrailingDirs) + .rewrite(DirIndex::unconditional("index.html")) + } + + /// Exactly like [`FileServer::new()`] except it _does not_ serve directory + /// index files via [`DirIndex`]. It rewrites with the following: + /// + /// - `|f, _| f.is_visible()`: Serve only visible files (hide dotfiles). + /// - [`Prefix::checked(path)`]: Prefix requests with `path`. + /// - [`TrailingDirs`]: Ensure directory have a trailing slash. + /// + /// # Example + /// + /// Constructs a default file server to serve files from `./static` using + /// `index.txt` as the index file if `index.html` doesn't exist. + /// + /// ```rust,no_run + /// # #[macro_use] extern crate rocket; + /// use rocket::fs::{FileServer, rewrite::DirIndex}; + /// + /// #[launch] + /// fn rocket() -> _ { + /// let server = FileServer::new("static") + /// .rewrite(DirIndex::if_exists("index.html")) + /// .rewrite(DirIndex::unconditional("index.txt")); + /// + /// rocket::build() + /// .mount("/", server) + /// } + /// ``` + /// + /// [`Prefix::checked(path)`]: crate::fs::rewrite::Prefix::checked + /// [`TrailingDirs`]: crate::fs::rewrite::TrailingDirs + pub fn without_index>(path: P) -> Self { + Self::identity() + .filter(|f, _| f.is_visible()) + .rewrite(Prefix::checked(path)) + .rewrite(TrailingDirs) + } + + /// Constructs a new `FileServer` with no rewrites. + /// + /// Without any rewrites, a `FileServer` will try to serve the requested + /// file from the current working directory. In other words, it represents + /// the identity rewrite. For example, a request `GET /foo/bar` will be + /// passed through unmodified and thus `./foo/bar` will be served. This is + /// very unlikely to be what you want. + /// + /// Prefer to use [`FileServer::new()`] or [`FileServer::without_index()`] + /// whenever possible and otherwise use one or more of the rewrites in + /// [`rocket::fs::rewrite`] or your own custom rewrites. + /// + /// # Example + /// + /// ```rust,no_run + /// # #[macro_use] extern crate rocket; + /// use rocket::fs::{FileServer, rewrite}; + /// + /// #[launch] + /// fn rocket() -> _ { + /// // A file server that serves exactly one file: /www/foo.html. The + /// // file is served irrespective of what's requested. + /// let server = FileServer::identity() + /// .rewrite(rewrite::File::checked("/www/foo.html")); + /// + /// rocket::build() + /// .mount("/", server) + /// } + /// ``` + pub fn identity() -> Self { Self { rewrites: vec![], - rank, + rank: Self::DEFAULT_RANK } } - /// Constructs a new `FileServer`, with the defualt rank of 10. - /// - /// See [`FileServer::new`]. - pub fn from>(path: P) -> Self { - Self::new(path, Self::DEFAULT_RANK) - } - - /// Constructs a new `FileServer` that serves files from the file system - /// `path`, with the specified rank. - /// - /// Adds a set of default rewrites: - /// - [`filter_dotfiles`]: Hides all dotfiles. - /// - [`dir_root(path)`](dir_root): Applies the root path. - /// - [`normalize_dirs`]: Normalizes directories to have a trailing slash. - /// - [`index("index.html")`](index): Appends `index.html` to directory requests. - pub fn new>(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. + /// Sets the rank of the route emitted by the `FileServer` to `rank`. /// /// # Example /// - /// Redirects all requests that have been filtered to the root of the `FileServer`. /// ```rust,no_run - /// # use rocket::{fs::{FileServer, FileResponse}, response::Redirect, - /// # uri, Build, Rocket, Request}; - /// fn redir_missing<'p, 'h>(p: Option>, _req: &Request<'_>) - /// -> Option> - /// { - /// match p { - /// None => Redirect::temporary(uri!("/")).into(), - /// p => p, - /// } + /// # 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>, _req: &Request<'_>) -> Option> { + /// Some(p.unwrap_or_else(|| Redirect::temporary(uri!("/")).into())) /// } /// - /// # fn launch() -> Rocket { - /// rocket::build() - /// .mount("/", FileServer::from("static").and_rewrite(redir_missing)) - /// # } + /// #[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 and_rewrite(mut self, f: impl Rewriter) -> Self { - self.rewrites.push(Arc::new(f)); + pub fn rewrite(mut self, rewriter: R) -> Self { + self.rewrites.push(Arc::new(rewriter)); self } - /// Filter what files this `FileServer` will respond with + /// Adds a rewriter to the pipeline that returns `Some` only when the + /// function `f` returns `true`, filtering out all other files. /// /// # Example /// - /// Filter out all paths with a filename of `hidden`. + /// Allow all files that don't have a file name or have a file name other + /// than "hidden". + /// /// ```rust,no_run - /// # use rocket::{fs::FileServer, response::Redirect, uri, Rocket, Build}; - /// # fn launch() -> Rocket { - /// rocket::build() - /// .mount( - /// "/", - /// FileServer::from("static") - /// .filter_file(|f, _r| f.path.file_name() != Some("hidden".as_ref())) - /// ) - /// # } + /// # #[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_file(self, f: F) -> Self - where F: Fn(&File<'_, '_>, &Request<'_>) -> bool + Send + Sync + 'static + pub fn filter(self, f: F) -> Self + where F: Fn(&File<'_>, &Request<'_>) -> bool { - self.and_rewrite(FilterFile(f)) + struct Filter(F); + + impl Rewriter for Filter + where F: Fn(&File<'_>, &Request<'_>) -> bool + Send + Sync + 'static + { + fn rewrite<'r>(&self, f: Option>, r: &Request<'_>) -> Option> { + f.and_then(|f| match f { + Rewrite::File(f) if self.0(&f, r) => Some(Rewrite::File(f)), + _ => None, + }) + } + } + + self.rewrite(Filter(f)) } - /// Transform files + /// Adds a rewriter to the pipeline that maps the current `File` to another + /// `Rewrite` using `f`. If the current `Rewrite` is a `Redirect`, it is + /// passed through without calling `f`. /// /// # Example /// - /// Append `hidden` to the path of every file returned. + /// Append `index.txt` to every path. + /// /// ```rust,no_run - /// # use rocket::{fs::FileServer, Build, Rocket}; - /// # fn launch() -> Rocket { - /// rocket::build() - /// .mount( - /// "/", - /// FileServer::from("static") - /// .map_file(|f, _r| f.map_path(|p| p.join("hidden")).into()) - /// ) - /// # } + /// # #[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_file(self, f: F) -> Self - where F: for<'r, 'h> Fn(File<'r, 'h>, &Request<'_>) - -> FileResponse<'r, 'h> + Send + Sync + 'static + pub fn map(self, f: F) -> Self + where F: for<'r> Fn(File<'r>, &Request<'_>) -> Rewrite<'r> { - self.and_rewrite(MapFile(f)) + struct Map(F); + + impl Rewriter for Map + where F: for<'r> Fn(File<'r>, &Request<'_>) -> Rewrite<'r> + Send + Sync + 'static + { + fn rewrite<'r>(&self, f: Option>, r: &Request<'_>) -> Option> { + f.map(|f| match f { + Rewrite::File(f) => self.0(f, r), + Rewrite::Redirect(r) => Rewrite::Redirect(r), + }) + } + } + + self.rewrite(Map(f)) } } impl From for Vec { fn from(server: FileServer) -> Self { - // let source = figment::Source::File(server.root.clone()); let mut route = Route::ranked(server.rank, Method::Get, "/", server); - // I'd like to provide a more descriptive name, but we can't get more - // information out of `dyn Rewriter` route.name = Some("FileServer".into()); 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 as UriPath; let path: Option = req.segments::>(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(), - })); + let mut response = path.map(|p| Rewrite::File(File::new(p))); for rewrite in &self.rewrites { response = rewrite.rewrite(response, req); } - match response { - Some(FileResponse::File(file)) => file.respond_to(req, data).await, - Some(FileResponse::Redirect(r)) => { - r.respond_to(req) - .or_forward((data, Status::InternalServerError)) - }, - None => Outcome::forward(data, Status::NotFound), - } + 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)) } } -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) - } - }; +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> { + 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) } } diff --git a/core/lib/src/response/redirect.rs b/core/lib/src/response/redirect.rs index 6738c45f..7b5cf5d2 100644 --- a/core/lib/src/response/redirect.rs +++ b/core/lib/src/response/redirect.rs @@ -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>); impl Redirect { diff --git a/core/lib/src/response/response.rs b/core/lib/src/response/response.rs index 7abdaeca..a3b49cec 100644 --- a/core/lib/src/response/response.rs +++ b/core/lib/src/response/response.rs @@ -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
`. See [trait diff --git a/core/lib/tests/file_server.rs b/core/lib/tests/file_server.rs index 1802d48f..5de10fb5 100644 --- a/core/lib/tests/file_server.rs +++ b/core/lib/tests/file_server.rs @@ -1,19 +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::{ - dir_root, - file_root, - filter_dotfiles, - index, - file_root_permissive, - normalize_dirs, - relative, - FileServer -}; +use rocket::fs::{FileServer, relative, rewrite::*}; fn static_root() -> &'static Path { Path::new(relative!("/tests/static")) @@ -22,57 +13,65 @@ fn static_root() -> &'static Path { fn rocket() -> Rocket { let root = static_root(); rocket::build() - .mount("/default", FileServer::from(&root)) + .mount("/default", FileServer::new(&root)) .mount( "/no_index", - FileServer::empty() - .filter_file(filter_dotfiles) - .map_file(dir_root(&root)) + FileServer::identity() + .filter(|f, _| f.is_visible()) + .rewrite(Prefix::checked(&root)) ) .mount( "/dots", - FileServer::empty() - .map_file(dir_root(&root)) + FileServer::identity() + .rewrite(Prefix::checked(&root)) ) .mount( "/index", - FileServer::empty() - .filter_file(filter_dotfiles) - .map_file(dir_root(&root)) - .map_file(index("index.html")) + 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::empty() - .map_file(dir_root(&root)) - .map_file(index("index.html")) + FileServer::identity() + .rewrite(Prefix::checked(&root)) + .rewrite(DirIndex::unconditional("index.html")) ) .mount( "/redir", - FileServer::empty() - .filter_file(filter_dotfiles) - .map_file(dir_root(&root)) - .map_file(normalize_dirs) + FileServer::identity() + .filter(|f, _| f.is_visible()) + .rewrite(Prefix::checked(&root)) + .rewrite(TrailingDirs) ) .mount( "/redir_index", - FileServer::empty() - .filter_file(filter_dotfiles) - .map_file(dir_root(&root)) - .map_file(normalize_dirs) - .map_file(index("index.html")) + FileServer::identity() + .filter(|f, _| f.is_visible()) + .rewrite(Prefix::checked(&root)) + .rewrite(TrailingDirs) + .rewrite(DirIndex::unconditional("index.html")) ) .mount( "/index_file", - FileServer::empty() - .filter_file(filter_dotfiles) - .map_file(file_root(root.join("other/hello.txt"))) + FileServer::identity() + .filter(|f, _| f.is_visible()) + .rewrite(File::checked(root.join("other/hello.txt"))) ) .mount( "/missing_root", - FileServer::empty() - .filter_file(filter_dotfiles) - .map_file(file_root_permissive(root.join("no_file"))) + FileServer::identity() + .filter(|f, _| f.is_visible()) + .rewrite(File::new(root.join("no_file"))) ) } @@ -81,6 +80,7 @@ static REGULAR_FILES: &[&str] = &[ "inner/goodbye", "inner/index.html", "other/hello.txt", + "other/index.htm", ]; static HIDDEN_FILES: &[&str] = &[ @@ -104,7 +104,7 @@ fn assert_file_matches(client: &Client, prefix: &str, path: &str, disk_path: Opt path = path.join("index.html"); } - 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)); @@ -179,12 +179,19 @@ fn test_allow_special_dotpaths() { 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, rank); - let b = FileServer::new(&root, rank); + let a = FileServer::new(&root).rank(rank); + let b = FileServer::new(&root).rank(rank); for handler in vec![a, b] { let routes: Vec = handler.into(); @@ -231,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. @@ -257,32 +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_root(static_root().join("missing_file")); + let _ = File::checked(static_root().join("missing_file")); } #[test] #[should_panic] fn test_panic_on_missing_dir() { - let _ = dir_root(static_root().join("missing_dir")); + let _ = Prefix::checked(static_root().join("missing_dir")); } #[test] #[should_panic] fn test_panic_on_file_not_dir() { - let _ = dir_root(static_root().join("index.html")); + let _ = Prefix::checked(static_root().join("index.html")); } diff --git a/core/lib/tests/static/inner/index.html b/core/lib/tests/static/inner/index.html index 1810989f..faf61358 100644 --- a/core/lib/tests/static/inner/index.html +++ b/core/lib/tests/static/inner/index.html @@ -1 +1 @@ -Inner index. +Inner index.html diff --git a/core/lib/tests/static/other/index.htm b/core/lib/tests/static/other/index.htm new file mode 100644 index 00000000..6048ea0a --- /dev/null +++ b/core/lib/tests/static/other/index.htm @@ -0,0 +1 @@ +Inner index.htm diff --git a/docs/guide/05-requests.md b/docs/guide/05-requests.md index 838cbb12..08a0329d 100644 --- a/docs/guide/05-requests.md +++ b/docs/guide/05-requests.md @@ -169,7 +169,7 @@ async fn files(file: PathBuf) -> Option { fn rocket() -> _ { rocket::build() // serve files from `/www/static` at path `/public` - .mount("/public", FileServer::from("/www/static")) + .mount("/public", FileServer::new("/www/static")) } ``` diff --git a/docs/guide/11-deploying.md b/docs/guide/11-deploying.md index 5afd1898..245683cc 100644 --- a/docs/guide/11-deploying.md +++ b/docs/guide/11-deploying.md @@ -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()) } ``` diff --git a/examples/chat/src/main.rs b/examples/chat/src/main.rs index 0a2f68d5..eab8abe1 100644 --- a/examples/chat/src/main.rs +++ b/examples/chat/src/main.rs @@ -54,5 +54,5 @@ fn rocket() -> _ { rocket::build() .manage(channel::(1024).0) .mount("/", routes![post, events]) - .mount("/", FileServer::from(relative!("static"))) + .mount("/", FileServer::new(relative!("static"))) } diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs index a908eb47..5953403a 100644 --- a/examples/forms/src/main.rs +++ b/examples/forms/src/main.rs @@ -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"))) } diff --git a/examples/static-files/src/main.rs b/examples/static-files/src/main.rs index 53c42708..7b94e7df 100644 --- a/examples/static-files/src/main.rs +++ b/examples/static-files/src/main.rs @@ -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"))) } diff --git a/examples/static-files/src/tests.rs b/examples/static-files/src/tests.rs index 59c0ceca..824e157f 100644 --- a/examples/static-files/src/tests.rs +++ b/examples/static-files/src/tests.rs @@ -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); diff --git a/examples/todo/src/main.rs b/examples/todo/src/main.rs index a12f7ab4..b6411624 100644 --- a/examples/todo/src/main.rs +++ b/examples/todo/src/main.rs @@ -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]) } diff --git a/examples/upgrade/src/main.rs b/examples/upgrade/src/main.rs index 7526b5b1..c9a69bd3 100644 --- a/examples/upgrade/src/main.rs +++ b/examples/upgrade/src/main.rs @@ -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"))) }