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}