rocket/fs/
server.rs

1use std::fmt;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4use std::borrow::Cow;
5
6use crate::{response, Data, Request, Response};
7use crate::outcome::IntoOutcome;
8use crate::http::{uri::Segments, HeaderMap, Method, ContentType, Status};
9use crate::route::{Route, Handler, Outcome};
10use crate::response::Responder;
11use crate::util::Formatter;
12use crate::fs::rewrite::*;
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;
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;
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;
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;
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;
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;
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    /// # use rocket::fs::FileServer;
209    /// # fn make_server() -> FileServer {
210    /// FileServer::identity()
211    ///    .rank(5)
212    /// # }
213    pub fn rank(mut self, rank: isize) -> Self {
214        self.rank = rank;
215        self
216    }
217
218    /// Add `rewriter` to the rewrite pipeline.
219    ///
220    /// # Example
221    ///
222    /// Redirect filtered requests (`None`) to `/`.
223    ///
224    /// ```rust,no_run
225    /// # #[macro_use] extern crate rocket;
226    /// use rocket::fs::{FileServer, rewrite::Rewrite};
227    /// use rocket::{request::Request, response::Redirect};
228    ///
229    /// fn redir_missing<'r>(p: Option<Rewrite<'r>>, _req: &Request<'_>) -> Option<Rewrite<'r>> {
230    ///     Some(p.unwrap_or_else(|| Redirect::temporary(uri!("/")).into()))
231    /// }
232    ///
233    /// #[launch]
234    /// fn rocket() -> _ {
235    ///     rocket::build()
236    ///         .mount("/", FileServer::new("static").rewrite(redir_missing))
237    /// }
238    /// ```
239    ///
240    /// Note that `redir_missing` is not a closure in this example. Making it a closure
241    /// causes compilation to fail with a lifetime error. It really shouldn't but it does.
242    pub fn rewrite<R: Rewriter>(mut self, rewriter: R) -> Self {
243        self.rewrites.push(Arc::new(rewriter));
244        self
245    }
246
247    /// Adds a rewriter to the pipeline that returns `Some` only when the
248    /// function `f` returns `true`, filtering out all other files.
249    ///
250    /// # Example
251    ///
252    /// Allow all files that don't have a file name or have a file name other
253    /// than "hidden".
254    ///
255    /// ```rust,no_run
256    /// # #[macro_use] extern crate rocket;
257    /// use rocket::fs::FileServer;
258    ///
259    /// #[launch]
260    /// fn rocket() -> _ {
261    ///     let server = FileServer::new("static")
262    ///         .filter(|f, _| f.path.file_name() != Some("hidden".as_ref()));
263    ///
264    ///     rocket::build()
265    ///         .mount("/", server)
266    /// }
267    /// ```
268    pub fn filter<F: Send + Sync + 'static>(self, f: F) -> Self
269        where F: Fn(&File<'_>, &Request<'_>) -> bool
270    {
271        struct Filter<F>(F);
272
273        impl<F> Rewriter for Filter<F>
274            where F: Fn(&File<'_>, &Request<'_>) -> bool + Send + Sync + 'static
275        {
276            fn rewrite<'r>(&self, f: Option<Rewrite<'r>>, r: &Request<'_>) -> Option<Rewrite<'r>> {
277                f.and_then(|f| match f {
278                    Rewrite::File(f) if self.0(&f, r) => Some(Rewrite::File(f)),
279                    _ => None,
280                })
281            }
282        }
283
284        self.rewrite(Filter(f))
285    }
286
287    /// Adds a rewriter to the pipeline that maps the current `File` to another
288    /// `Rewrite` using `f`. If the current `Rewrite` is a `Redirect`, it is
289    /// passed through without calling `f`.
290    ///
291    /// # Example
292    ///
293    /// Append `index.txt` to every path.
294    ///
295    /// ```rust,no_run
296    /// # #[macro_use] extern crate rocket;
297    /// use rocket::fs::FileServer;
298    ///
299    /// #[launch]
300    /// fn rocket() -> _ {
301    ///     let server = FileServer::new("static")
302    ///         .map(|f, _| f.map_path(|p| p.join("index.txt")).into());
303    ///
304    ///     rocket::build()
305    ///         .mount("/", server)
306    /// }
307    /// ```
308    pub fn map<F: Send + Sync + 'static>(self, f: F) -> Self
309        where F: for<'r> Fn(File<'r>, &Request<'_>) -> Rewrite<'r>
310    {
311        struct Map<F>(F);
312
313        impl<F> Rewriter for Map<F>
314            where F: for<'r> Fn(File<'r>, &Request<'_>) -> Rewrite<'r> + Send + Sync + 'static
315        {
316            fn rewrite<'r>(&self, f: Option<Rewrite<'r>>, r: &Request<'_>) -> Option<Rewrite<'r>> {
317                f.map(|f| match f {
318                    Rewrite::File(f) => self.0(f, r),
319                    Rewrite::Redirect(r) => Rewrite::Redirect(r),
320                })
321            }
322        }
323
324        self.rewrite(Map(f))
325    }
326}
327
328impl From<FileServer> for Vec<Route> {
329    fn from(server: FileServer) -> Self {
330        let mut route = Route::ranked(server.rank, Method::Get, "/<path..>", server);
331        route.name = Some("FileServer".into());
332        vec![route]
333    }
334}
335
336#[crate::async_trait]
337impl Handler for FileServer {
338    async fn handle<'r>(&self, req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r> {
339        use crate::http::uri::fmt::Path as UriPath;
340        let path: Option<PathBuf> = req.segments::<Segments<'_, UriPath>>(0..).ok()
341            .and_then(|segments| segments.to_path_buf(true).ok());
342
343        let mut response = path.map(|p| Rewrite::File(File::new(p)));
344        for rewrite in &self.rewrites {
345            response = rewrite.rewrite(response, req);
346        }
347
348        let (outcome, status) = match response {
349            Some(Rewrite::File(f)) => (f.open().await.respond_to(req), Status::NotFound),
350            Some(Rewrite::Redirect(r)) => (r.respond_to(req), Status::InternalServerError),
351            None => return Outcome::forward(data, Status::NotFound),
352        };
353
354        outcome.or_forward((data, status))
355    }
356}
357
358impl fmt::Debug for FileServer {
359    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360        f.debug_struct("FileServer")
361            .field("rewrites", &Formatter(|f| write!(f, "<{} rewrites>", self.rewrites.len())))
362            .field("rank", &self.rank)
363            .finish()
364    }
365}
366
367impl<'r> File<'r> {
368    async fn open(self) -> std::io::Result<NamedFile<'r>> {
369        let file = tokio::fs::File::open(&self.path).await?;
370        let metadata = file.metadata().await?;
371        if metadata.is_dir() {
372            return Err(std::io::Error::other("is a directory"));
373        }
374
375        Ok(NamedFile {
376            file,
377            len: metadata.len(),
378            path: self.path,
379            headers: self.headers,
380        })
381    }
382}
383
384struct NamedFile<'r> {
385    file: tokio::fs::File,
386    len: u64,
387    path: Cow<'r, Path>,
388    headers: HeaderMap<'r>,
389}
390
391// Do we want to allow the user to rewrite the Content-Type?
392impl<'r> Responder<'r, 'r> for NamedFile<'r> {
393    fn respond_to(self, _: &'r Request<'_>) -> response::Result<'r> {
394        let mut response = Response::new();
395        response.set_header_map(self.headers);
396        if !response.headers().contains("Content-Type") {
397            self.path.extension()
398                .and_then(|ext| ext.to_str())
399                .and_then(ContentType::from_extension)
400                .map(|content_type| response.set_header(content_type));
401        }
402
403        response.set_sized_body(self.len as usize, self.file);
404        Ok(response)
405    }
406}