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}