rocket/fs/
rewrite.rs

1use std::borrow::Cow;
2use std::path::{Path, PathBuf};
3
4use crate::Request;
5use crate::http::{ext::IntoOwned, HeaderMap};
6use crate::response::Redirect;
7
8/// A file server [`Rewrite`] rewriter.
9///
10/// A [`FileServer`] is a sequence of [`Rewriter`]s which transform the incoming
11/// request path into a [`Rewrite`] or `None`. The first rewriter is called with
12/// the request path as a [`Rewrite::File`]. Each `Rewriter` thereafter is
13/// called in-turn with the previously returned [`Rewrite`], and the value
14/// returned from the last `Rewriter` is used to respond to the request. If the
15/// final rewrite is `None` or a nonexistent path or a directory, [`FileServer`]
16/// responds with [`Status::NotFound`]. Otherwise it responds with the file
17/// contents, if [`Rewrite::File`] is specified, or a redirect, if
18/// [`Rewrite::Redirect`] is specified.
19///
20/// [`FileServer`]: super::FileServer
21/// [`Status::NotFound`]: crate::http::Status::NotFound
22pub trait Rewriter: Send + Sync + 'static {
23    /// Alter the [`Rewrite`] as needed.
24    fn rewrite<'r>(&self, opt: Option<Rewrite<'r>>, req: &'r Request<'_>) -> Option<Rewrite<'r>>;
25}
26
27/// A Response from a [`FileServer`](super::FileServer)
28#[derive(Debug, Clone)]
29#[non_exhaustive]
30pub enum Rewrite<'r> {
31    /// Return the contents of the specified file.
32    File(File<'r>),
33    /// Returns a Redirect.
34    Redirect(Redirect),
35}
36
37/// A File response from a [`FileServer`](super::FileServer) and a rewriter.
38#[derive(Debug, Clone)]
39#[non_exhaustive]
40pub struct File<'r> {
41    /// The path to the file that [`FileServer`](super::FileServer) will respond with.
42    pub path: Cow<'r, Path>,
43    /// A list of headers to be added to the generated response.
44    pub headers: HeaderMap<'r>,
45}
46
47impl<'r> File<'r> {
48    /// A new `File`, with not additional headers.
49    pub fn new(path: impl Into<Cow<'r, Path>>) -> Self {
50        Self { path: path.into(), headers: HeaderMap::new() }
51    }
52
53    /// A new `File`, with not additional headers.
54    ///
55    /// # Panics
56    ///
57    /// Panics if the `path` does not exist.
58    pub fn checked<P: AsRef<Path>>(path: P) -> Self {
59        let path = path.as_ref();
60        if !path.exists() {
61            let path = path.display();
62            error!(%path, "FileServer path does not exist.\n\
63                Panicking to prevent inevitable handler error.");
64            panic!("missing file {}: refusing to continue", path);
65        }
66
67        Self::new(path.to_path_buf())
68    }
69
70    /// Replace the path in `self` with the result of applying `f` to the path.
71    pub fn map_path<F, P>(self, f: F) -> Self
72        where F: FnOnce(Cow<'r, Path>) -> P, P: Into<Cow<'r, Path>>,
73    {
74        Self {
75            path: f(self.path).into(),
76            headers: self.headers,
77        }
78    }
79
80    /// Returns `true` if the file is a dotfile. A dotfile is a file whose
81    /// name or any directory in it's path start with a period (`.`) and is
82    /// considered hidden.
83    ///
84    /// # Windows Note
85    ///
86    /// This does *not* check the file metadata on any platform, so hidden files
87    /// on Windows will not be detected by this method.
88    pub fn is_hidden(&self) -> bool {
89        self.path.iter().any(|s| s.as_encoded_bytes().starts_with(b"."))
90    }
91
92    /// Returns `true` if the file is not hidden. This is the inverse of
93    /// [`File::is_hidden()`].
94    pub fn is_visible(&self) -> bool {
95        !self.is_hidden()
96    }
97}
98
99/// Prefixes all paths with a given path.
100///
101/// # Example
102///
103/// ```rust,no_run
104/// use rocket::fs::FileServer;
105/// use rocket::fs::rewrite::Prefix;
106///
107/// FileServer::identity()
108///    .filter(|f, _| f.is_visible())
109///    .rewrite(Prefix::checked("static"));
110/// ```
111pub struct Prefix(PathBuf);
112
113impl Prefix {
114    /// Panics if `path` does not exist.
115    pub fn checked<P: AsRef<Path>>(path: P) -> Self {
116        let path = path.as_ref();
117        if !path.is_dir() {
118            let path = path.display();
119            error!(%path, "FileServer path is not a directory.");
120            warn!("Aborting early to prevent inevitable handler error.");
121            panic!("invalid directory: refusing to continue");
122        }
123
124        Self(path.to_path_buf())
125    }
126
127    /// Creates a new `Prefix` from a path.
128    pub fn unchecked<P: AsRef<Path>>(path: P) -> Self {
129        Self(path.as_ref().to_path_buf())
130    }
131}
132
133impl Rewriter for Prefix {
134    fn rewrite<'r>(&self, opt: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
135        opt.map(|r| match r {
136            Rewrite::File(f) => Rewrite::File(f.map_path(|p| self.0.join(p))),
137            Rewrite::Redirect(r) => Rewrite::Redirect(r),
138        })
139    }
140}
141
142impl Rewriter for PathBuf {
143    fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
144        Some(Rewrite::File(File::new(self.clone())))
145    }
146}
147
148/// Normalize directories to always include a trailing slash by redirecting
149/// (with a 302 temporary redirect) requests for directories without a trailing
150/// slash to the same path with a trailing slash.
151///
152/// # Example
153///
154/// ```rust,no_run
155/// use rocket::fs::FileServer;
156/// use rocket::fs::rewrite::{Prefix, TrailingDirs};
157///
158/// FileServer::identity()
159///     .filter(|f, _| f.is_visible())
160///     .rewrite(TrailingDirs);
161/// ```
162pub struct TrailingDirs;
163
164impl Rewriter for TrailingDirs {
165    fn rewrite<'r>(&self, opt: Option<Rewrite<'r>>, req: &Request<'_>) -> Option<Rewrite<'r>> {
166        if let Some(Rewrite::File(f)) = &opt {
167            if !req.uri().path().ends_with('/') && f.path.is_dir() {
168                let uri = req.uri().clone().into_owned();
169                let uri = uri.map_path(|p| format!("{p}/")).unwrap();
170                return Some(Rewrite::Redirect(Redirect::temporary(uri)));
171            }
172        }
173
174        opt
175    }
176}
177
178/// Rewrite a directory to a file inside of that directory.
179///
180/// # Example
181///
182/// Rewrites all directory requests to `directory/index.html`.
183///
184/// ```rust,no_run
185/// use rocket::fs::FileServer;
186/// use rocket::fs::rewrite::DirIndex;
187///
188/// FileServer::without_index("static")
189///     .rewrite(DirIndex::if_exists("index.htm"))
190///     .rewrite(DirIndex::unconditional("index.html"));
191/// ```
192pub struct DirIndex {
193    path: PathBuf,
194    check: bool,
195}
196
197impl DirIndex {
198    /// Appends `path` to every request for a directory.
199    pub fn unconditional(path: impl AsRef<Path>) -> Self {
200        Self { path: path.as_ref().to_path_buf(), check: false }
201    }
202
203    /// Only appends `path` to a request for a directory if the file exists.
204    pub fn if_exists(path: impl AsRef<Path>) -> Self {
205        Self { path: path.as_ref().to_path_buf(), check: true }
206    }
207}
208
209impl Rewriter for DirIndex {
210    fn rewrite<'r>(&self, opt: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
211        match opt? {
212            Rewrite::File(f) if f.path.is_dir() => {
213                let candidate = f.path.join(&self.path);
214                if self.check && !candidate.is_file() {
215                    return Some(Rewrite::File(f));
216                }
217
218                Some(Rewrite::File(f.map_path(|_| candidate)))
219            }
220            r => Some(r),
221        }
222    }
223}
224
225impl<'r> From<File<'r>> for Rewrite<'r> {
226    fn from(value: File<'r>) -> Self {
227        Self::File(value)
228    }
229}
230
231impl<'r> From<Redirect> for Rewrite<'r> {
232    fn from(value: Redirect) -> Self {
233        Self::Redirect(value)
234    }
235}
236
237impl<F: Send + Sync + 'static> Rewriter for F
238    where F: for<'r> Fn(Option<Rewrite<'r>>, &Request<'_>) -> Option<Rewrite<'r>>
239{
240    fn rewrite<'r>(&self, f: Option<Rewrite<'r>>, r: &Request<'_>) -> Option<Rewrite<'r>> {
241        self(f, r)
242    }
243}
244
245impl Rewriter for Rewrite<'static> {
246    fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
247        Some(self.clone())
248    }
249}
250
251impl Rewriter for File<'static> {
252    fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
253        Some(Rewrite::File(self.clone()))
254    }
255}
256
257impl Rewriter for Redirect {
258    fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> {
259        Some(Rewrite::Redirect(self.clone()))
260    }
261}