rocket_community/fs/
rewrite.rs

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