rocket_community/fs/
server.rs

1use std::borrow::Cow;
2use std::fmt;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use crate::fs::rewrite::*;
7use crate::http::{uri::Segments, ContentType, HeaderMap, Method, Status};
8use crate::outcome::IntoOutcome;
9use crate::response::Responder;
10use crate::route::{Handler, Outcome, Route};
11use crate::util::Formatter;
12use crate::{response, Data, Request, Response};
13
14/// Custom handler for serving static files.
15///
16/// This handler makes is simple to serve static files from a directory on the
17/// local file system. To use it, construct a `FileServer` using
18/// [`FileServer::new()`], then `mount` the handler.
19///
20/// ```rust,no_run
21/// # #[macro_use] extern crate rocket_community as rocket;
22/// use rocket::fs::FileServer;
23///
24/// #[launch]
25/// fn rocket() -> _ {
26///     rocket::build()
27///         .mount("/", FileServer::new("/www/static"))
28/// }
29/// ```
30///
31/// When mounted, the handler serves files from the specified path. If a
32/// requested file does not exist, the handler _forwards_ the request with a
33/// `404` status.
34///
35/// By default, the route has a rank of `10` which can be changed with
36/// [`FileServer::rank()`].
37///
38/// # Customization
39///
40/// `FileServer` works through a pipeline of _rewrites_ in which a requested
41/// path is transformed into a `PathBuf` via [`Segments::to_path_buf()`] and
42/// piped through a series of [`Rewriter`]s to obtain a final [`Rewrite`] which
43/// is then used to generate a final response. See [`Rewriter`] for complete
44/// details on implementing your own `Rewriter`s.
45///
46/// # Example
47///
48/// Serve files from the `/static` directory on the local file system at the
49/// `/public` path:
50///
51/// ```rust,no_run
52/// # #[macro_use] extern crate rocket_community as rocket;
53/// use rocket::fs::FileServer;
54///
55/// #[launch]
56/// fn rocket() -> _ {
57///     rocket::build().mount("/public", FileServer::new("/static"))
58/// }
59/// ```
60///
61/// Requests for files at `/public/<path..>` will be handled by returning the
62/// contents of `/static/<path..>`. Requests for directories will return the
63/// contents of `index.html`.
64///
65/// ## Relative Paths
66///
67/// In the example above, `/static` is an absolute path. If your static files
68/// are stored relative to your crate and your project is managed by Cargo, use
69/// the [`relative!`] macro to obtain a path that is relative to your crate's
70/// root. For example, to serve files in the `static` subdirectory of your crate
71/// at `/`, you might write:
72///
73/// ```rust,no_run
74/// # #[macro_use] extern crate rocket_community as rocket;
75/// use rocket::fs::{FileServer, relative};
76///
77/// #[launch]
78/// fn rocket() -> _ {
79///     rocket::build().mount("/", FileServer::new(relative!("static")))
80/// }
81/// ```
82///
83/// [`relative!`]: crate::fs::relative!
84#[derive(Clone)]
85pub struct FileServer {
86    rewrites: Vec<Arc<dyn Rewriter>>,
87    rank: isize,
88}
89
90impl FileServer {
91    /// The default rank use by `FileServer` routes.
92    const DEFAULT_RANK: isize = 10;
93
94    /// Constructs a new `FileServer` that serves files from the file system
95    /// `path` with the following rewrites:
96    ///
97    /// - `|f, _| f.is_visible()`: Serve only visible files (hide dotfiles).
98    /// - [`Prefix::checked(path)`]: Prefix requests with `path`.
99    /// - [`TrailingDirs`]: Ensure directory have a trailing slash.
100    /// - [`DirIndex::unconditional("index.html")`]: Serve `$dir/index.html` for
101    ///   requests to directory `$dir`.
102    ///
103    /// If you don't want to serve index files or want a different index file,
104    /// use [`Self::without_index`]. To customize the entire request to file
105    /// path rewrite pipeline, use [`Self::identity`].
106    ///
107    /// [`Prefix::checked(path)`]: crate::fs::rewrite::Prefix::checked
108    /// [`TrailingDirs`]: crate::fs::rewrite::TrailingDirs
109    /// [`DirIndex::unconditional("index.html")`]: DirIndex::unconditional()
110    ///
111    /// # Example
112    ///
113    /// ```rust,no_run
114    /// # #[macro_use] extern crate rocket_community as rocket;
115    /// use rocket::fs::FileServer;
116    ///
117    /// #[launch]
118    /// fn rocket() -> _ {
119    ///     rocket::build()
120    ///         .mount("/", FileServer::new("/www/static"))
121    /// }
122    /// ```
123    pub fn new<P: AsRef<Path>>(path: P) -> Self {
124        Self::identity()
125            .filter(|f, _| f.is_visible())
126            .rewrite(Prefix::checked(path))
127            .rewrite(TrailingDirs)
128            .rewrite(DirIndex::unconditional("index.html"))
129    }
130
131    /// Exactly like [`FileServer::new()`] except it _does not_ serve directory
132    /// index files via [`DirIndex`]. It rewrites with the following:
133    ///
134    /// - `|f, _| f.is_visible()`: Serve only visible files (hide dotfiles).
135    /// - [`Prefix::checked(path)`]: Prefix requests with `path`.
136    /// - [`TrailingDirs`]: Ensure directory have a trailing slash.
137    ///
138    /// # Example
139    ///
140    /// Constructs a default file server to serve files from `./static` using
141    /// `index.txt` as the index file if `index.html` doesn't exist.
142    ///
143    /// ```rust,no_run
144    /// # #[macro_use] extern crate rocket_community as rocket;
145    /// use rocket::fs::{FileServer, rewrite::DirIndex};
146    ///
147    /// #[launch]
148    /// fn rocket() -> _ {
149    ///     let server = FileServer::new("static")
150    ///         .rewrite(DirIndex::if_exists("index.html"))
151    ///         .rewrite(DirIndex::unconditional("index.txt"));
152    ///
153    ///     rocket::build()
154    ///         .mount("/", server)
155    /// }
156    /// ```
157    ///
158    /// [`Prefix::checked(path)`]: crate::fs::rewrite::Prefix::checked
159    /// [`TrailingDirs`]: crate::fs::rewrite::TrailingDirs
160    pub fn without_index<P: AsRef<Path>>(path: P) -> Self {
161        Self::identity()
162            .filter(|f, _| f.is_visible())
163            .rewrite(Prefix::checked(path))
164            .rewrite(TrailingDirs)
165    }
166
167    /// Constructs a new `FileServer` with no rewrites.
168    ///
169    /// Without any rewrites, a `FileServer` will try to serve the requested
170    /// file from the current working directory. In other words, it represents
171    /// the identity rewrite. For example, a request `GET /foo/bar` will be
172    /// passed through unmodified and thus `./foo/bar` will be served. This is
173    /// very unlikely to be what you want.
174    ///
175    /// Prefer to use [`FileServer::new()`] or [`FileServer::without_index()`]
176    /// whenever possible and otherwise use one or more of the rewrites in
177    /// [`rocket::fs::rewrite`] or your own custom rewrites.
178    ///
179    /// # Example
180    ///
181    /// ```rust,no_run
182    /// # #[macro_use] extern crate rocket_community as rocket;
183    /// use rocket::fs::{FileServer, rewrite};
184    ///
185    /// #[launch]
186    /// fn rocket() -> _ {
187    ///     // A file server that serves exactly one file: /www/foo.html. The
188    ///     // file is served irrespective of what's requested.
189    ///     let server = FileServer::identity()
190    ///         .rewrite(rewrite::File::checked("/www/foo.html"));
191    ///
192    ///     rocket::build()
193    ///         .mount("/", server)
194    /// }
195    /// ```
196    pub fn identity() -> Self {
197        Self {
198            rewrites: vec![],
199            rank: Self::DEFAULT_RANK,
200        }
201    }
202
203    /// Sets the rank of the route emitted by the `FileServer` to `rank`.
204    ///
205    /// # Example
206    ///
207    /// ```rust,no_run
208    /// # extern crate rocket_community as rocket;
209    /// # use rocket::fs::FileServer;
210    /// # fn make_server() -> FileServer {
211    /// FileServer::identity()
212    ///    .rank(5)
213    /// # }
214    pub fn rank(mut self, rank: isize) -> Self {
215        self.rank = rank;
216        self
217    }
218
219    /// Add `rewriter` to the rewrite pipeline.
220    ///
221    /// # Example
222    ///
223    /// Redirect filtered requests (`None`) to `/`.
224    ///
225    /// ```rust,no_run
226    /// # #[macro_use] extern crate rocket_community as rocket;
227    /// use rocket::fs::{FileServer, rewrite::Rewrite};
228    /// use rocket::{request::Request, response::Redirect};
229    ///
230    /// fn redir_missing<'r>(p: Option<Rewrite<'r>>, _req: &Request<'_>) -> Option<Rewrite<'r>> {
231    ///     Some(p.unwrap_or_else(|| Redirect::temporary(uri!("/")).into()))
232    /// }
233    ///
234    /// #[launch]
235    /// fn rocket() -> _ {
236    ///     rocket::build()
237    ///         .mount("/", FileServer::new("static").rewrite(redir_missing))
238    /// }
239    /// ```
240    ///
241    /// Note that `redir_missing` is not a closure in this example. Making it a closure
242    /// causes compilation to fail with a lifetime error. It really shouldn't but it does.
243    pub fn rewrite<R: Rewriter>(mut self, rewriter: R) -> Self {
244        self.rewrites.push(Arc::new(rewriter));
245        self
246    }
247
248    /// Adds a rewriter to the pipeline that returns `Some` only when the
249    /// function `f` returns `true`, filtering out all other files.
250    ///
251    /// # Example
252    ///
253    /// Allow all files that don't have a file name or have a file name other
254    /// than "hidden".
255    ///
256    /// ```rust,no_run
257    /// # #[macro_use] extern crate rocket_community as rocket;
258    /// use rocket::fs::FileServer;
259    ///
260    /// #[launch]
261    /// fn rocket() -> _ {
262    ///     let server = FileServer::new("static")
263    ///         .filter(|f, _| f.path.file_name() != Some("hidden".as_ref()));
264    ///
265    ///     rocket::build()
266    ///         .mount("/", server)
267    /// }
268    /// ```
269    pub fn filter<F: Send + Sync + 'static>(self, f: F) -> Self
270    where
271        F: Fn(&File<'_>, &Request<'_>) -> bool,
272    {
273        struct Filter<F>(F);
274
275        impl<F> Rewriter for Filter<F>
276        where
277            F: Fn(&File<'_>, &Request<'_>) -> bool + Send + Sync + 'static,
278        {
279            fn rewrite<'r>(&self, f: Option<Rewrite<'r>>, r: &Request<'_>) -> Option<Rewrite<'r>> {
280                f.and_then(|f| match f {
281                    Rewrite::File(f) if self.0(&f, r) => Some(Rewrite::File(f)),
282                    _ => None,
283                })
284            }
285        }
286
287        self.rewrite(Filter(f))
288    }
289
290    /// Adds a rewriter to the pipeline that maps the current `File` to another
291    /// `Rewrite` using `f`. If the current `Rewrite` is a `Redirect`, it is
292    /// passed through without calling `f`.
293    ///
294    /// # Example
295    ///
296    /// Append `index.txt` to every path.
297    ///
298    /// ```rust,no_run
299    /// # #[macro_use] extern crate rocket_community as rocket;
300    /// use rocket::fs::FileServer;
301    ///
302    /// #[launch]
303    /// fn rocket() -> _ {
304    ///     let server = FileServer::new("static")
305    ///         .map(|f, _| f.map_path(|p| p.join("index.txt")).into());
306    ///
307    ///     rocket::build()
308    ///         .mount("/", server)
309    /// }
310    /// ```
311    pub fn map<F: Send + Sync + 'static>(self, f: F) -> Self
312    where
313        F: for<'r> Fn(File<'r>, &Request<'_>) -> Rewrite<'r>,
314    {
315        struct Map<F>(F);
316
317        impl<F> Rewriter for Map<F>
318        where
319            F: for<'r> Fn(File<'r>, &Request<'_>) -> Rewrite<'r> + Send + Sync + 'static,
320        {
321            fn rewrite<'r>(&self, f: Option<Rewrite<'r>>, r: &Request<'_>) -> Option<Rewrite<'r>> {
322                f.map(|f| match f {
323                    Rewrite::File(f) => self.0(f, r),
324                    Rewrite::Redirect(r) => Rewrite::Redirect(r),
325                })
326            }
327        }
328
329        self.rewrite(Map(f))
330    }
331}
332
333impl From<FileServer> for Vec<Route> {
334    fn from(server: FileServer) -> Self {
335        let mut route = Route::ranked(server.rank, Method::Get, "/<path..>", server);
336        route.name = Some("FileServer".into());
337        vec![route]
338    }
339}
340
341#[crate::async_trait]
342impl Handler for FileServer {
343    async fn handle<'r>(&self, req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r> {
344        use crate::http::uri::fmt::Path as UriPath;
345        let path: Option<PathBuf> = req
346            .segments::<Segments<'_, UriPath>>(0..)
347            .ok()
348            .and_then(|segments| segments.to_path_buf(true).ok());
349
350        let mut response = path.map(|p| Rewrite::File(File::new(p)));
351        for rewrite in &self.rewrites {
352            response = rewrite.rewrite(response, req);
353        }
354
355        let (outcome, status) = match response {
356            Some(Rewrite::File(f)) => (f.open().await.respond_to(req), Status::NotFound),
357            Some(Rewrite::Redirect(r)) => (r.respond_to(req), Status::InternalServerError),
358            None => return Outcome::forward(data, Status::NotFound),
359        };
360
361        outcome.or_forward((data, status))
362    }
363}
364
365impl fmt::Debug for FileServer {
366    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367        f.debug_struct("FileServer")
368            .field(
369                "rewrites",
370                &Formatter(|f| write!(f, "<{} rewrites>", self.rewrites.len())),
371            )
372            .field("rank", &self.rank)
373            .finish()
374    }
375}
376
377impl<'r> File<'r> {
378    async fn open(self) -> std::io::Result<NamedFile<'r>> {
379        let file = tokio::fs::File::open(&self.path).await?;
380        let metadata = file.metadata().await?;
381        if metadata.is_dir() {
382            return Err(std::io::Error::other("is a directory"));
383        }
384
385        Ok(NamedFile {
386            file,
387            len: metadata.len(),
388            path: self.path,
389            headers: self.headers,
390        })
391    }
392}
393
394struct NamedFile<'r> {
395    file: tokio::fs::File,
396    len: u64,
397    path: Cow<'r, Path>,
398    headers: HeaderMap<'r>,
399}
400
401// Do we want to allow the user to rewrite the Content-Type?
402impl<'r> Responder<'r, 'r> for NamedFile<'r> {
403    fn respond_to(self, _: &'r Request<'_>) -> response::Result<'r> {
404        let mut response = Response::new();
405        response.set_header_map(self.headers);
406        if !response.headers().contains("Content-Type") {
407            self.path
408                .extension()
409                .and_then(|ext| ext.to_str())
410                .and_then(ContentType::from_extension)
411                .map(|content_type| response.set_header(content_type));
412        }
413
414        response.set_sized_body(self.len as usize, self.file);
415        Ok(response)
416    }
417}