rocket/fs/server.rs
1use std::path::{PathBuf, Path};
2
3use crate::{Request, Data};
4use crate::http::{Method, Status, uri::Segments, ext::IntoOwned};
5use crate::route::{Route, Handler, Outcome};
6use crate::response::{Redirect, Responder};
7use crate::outcome::IntoOutcome;
8use crate::fs::NamedFile;
9
10/// Custom handler for serving static files.
11///
12/// This handler makes it simple to serve static files from a directory on the
13/// local file system. To use it, construct a `FileServer` using either
14/// [`FileServer::from()`] or [`FileServer::new()`] then simply `mount` the
15/// handler at a desired path. When mounted, the handler will generate route(s)
16/// that serve the desired static files. If a requested file is not found, the
17/// routes _forward_ the incoming request. The default rank of the generated
18/// routes is `10`. To customize route ranking, use the [`FileServer::rank()`]
19/// method.
20///
21/// # Options
22///
23/// The handler's functionality can be customized by passing an [`Options`] to
24/// [`FileServer::new()`].
25///
26/// # Example
27///
28/// To serve files from the `/static` directory on the local file system at the
29/// `/public` path, allowing `index.html` files to be used to respond to
30/// requests for a directory (the default), you might write the following:
31///
32/// ```rust,no_run
33/// # #[macro_use] extern crate rocket;
34/// use rocket::fs::FileServer;
35///
36/// #[launch]
37/// fn rocket() -> _ {
38/// rocket::build().mount("/public", FileServer::from("/static"))
39/// }
40/// ```
41///
42/// With this, requests for files at `/public/<path..>` will be handled by
43/// returning the contents of `/static/<path..>`. Requests for _directories_ at
44/// `/public/<directory>` will be handled by returning the contents of
45/// `/static/<directory>/index.html`.
46///
47/// ## Relative Paths
48///
49/// In the example above, `/static` is an absolute path. If your static files
50/// are stored relative to your crate and your project is managed by Rocket, use
51/// the [`relative!`] macro to obtain a path that is relative to your
52/// crate's root. For example, to serve files in the `static` subdirectory of
53/// your crate at `/`, you might write:
54///
55/// ```rust,no_run
56/// # #[macro_use] extern crate rocket;
57/// use rocket::fs::{FileServer, relative};
58///
59/// #[launch]
60/// fn rocket() -> _ {
61/// rocket::build().mount("/", FileServer::from(relative!("static")))
62/// }
63/// ```
64#[derive(Debug, Clone)]
65pub struct FileServer {
66 root: PathBuf,
67 options: Options,
68 rank: isize,
69}
70
71impl FileServer {
72 /// The default rank use by `FileServer` routes.
73 const DEFAULT_RANK: isize = 10;
74
75 /// Constructs a new `FileServer` that serves files from the file system
76 /// `path`. By default, [`Options::Index`] is set, and the generated routes
77 /// have a rank of `10`. To serve static files with other options, use
78 /// [`FileServer::new()`]. To choose a different rank for generated routes,
79 /// use [`FileServer::rank()`].
80 ///
81 /// # Panics
82 ///
83 /// Panics if `path` does not exist or is not a directory.
84 ///
85 /// # Example
86 ///
87 /// Serve the static files in the `/www/public` local directory on path
88 /// `/static`.
89 ///
90 /// ```rust,no_run
91 /// # #[macro_use] extern crate rocket;
92 /// use rocket::fs::FileServer;
93 ///
94 /// #[launch]
95 /// fn rocket() -> _ {
96 /// rocket::build().mount("/static", FileServer::from("/www/public"))
97 /// }
98 /// ```
99 ///
100 /// Exactly as before, but set the rank for generated routes to `30`.
101 ///
102 /// ```rust,no_run
103 /// # #[macro_use] extern crate rocket;
104 /// use rocket::fs::FileServer;
105 ///
106 /// #[launch]
107 /// fn rocket() -> _ {
108 /// rocket::build().mount("/static", FileServer::from("/www/public").rank(30))
109 /// }
110 /// ```
111 #[track_caller]
112 pub fn from<P: AsRef<Path>>(path: P) -> Self {
113 FileServer::new(path, Options::default())
114 }
115
116 /// Constructs a new `FileServer` that serves files from the file system
117 /// `path` with `options` enabled. By default, the handler's routes have a
118 /// rank of `10`. To choose a different rank, use [`FileServer::rank()`].
119 ///
120 /// # Panics
121 ///
122 /// If [`Options::Missing`] is not set, panics if `path` does not exist or
123 /// is not a directory. Otherwise does not panic.
124 ///
125 /// # Example
126 ///
127 /// Serve the static files in the `/www/public` local directory on path
128 /// `/static` without serving index files or dot files. Additionally, serve
129 /// the same files on `/pub` with a route rank of -1 while also serving
130 /// index files and dot files.
131 ///
132 /// ```rust,no_run
133 /// # #[macro_use] extern crate rocket;
134 /// use rocket::fs::{FileServer, Options};
135 ///
136 /// #[launch]
137 /// fn rocket() -> _ {
138 /// let options = Options::Index | Options::DotFiles;
139 /// rocket::build()
140 /// .mount("/static", FileServer::from("/www/public"))
141 /// .mount("/pub", FileServer::new("/www/public", options).rank(-1))
142 /// }
143 /// ```
144 #[track_caller]
145 pub fn new<P: AsRef<Path>>(path: P, options: Options) -> Self {
146 use crate::yansi::Paint;
147
148 let path = path.as_ref();
149 if !options.contains(Options::Missing) {
150 if !options.contains(Options::IndexFile) && !path.is_dir() {
151 let path = path.display();
152 error!("FileServer path '{}' is not a directory.", path.primary());
153 warn_!("Aborting early to prevent inevitable handler error.");
154 panic!("invalid directory: refusing to continue");
155 } else if !path.exists() {
156 let path = path.display();
157 error!("FileServer path '{}' is not a file.", path.primary());
158 warn_!("Aborting early to prevent inevitable handler error.");
159 panic!("invalid file: refusing to continue");
160 }
161 }
162
163 FileServer { root: path.into(), options, rank: Self::DEFAULT_RANK }
164 }
165
166 /// Sets the rank for generated routes to `rank`.
167 ///
168 /// # Example
169 ///
170 /// ```rust,no_run
171 /// use rocket::fs::{FileServer, Options};
172 ///
173 /// // A `FileServer` created with `from()` with routes of rank `3`.
174 /// FileServer::from("/public").rank(3);
175 ///
176 /// // A `FileServer` created with `new()` with routes of rank `-15`.
177 /// FileServer::new("/public", Options::Index).rank(-15);
178 /// ```
179 pub fn rank(mut self, rank: isize) -> Self {
180 self.rank = rank;
181 self
182 }
183}
184
185impl From<FileServer> for Vec<Route> {
186 fn from(server: FileServer) -> Self {
187 let source = figment::Source::File(server.root.clone());
188 let mut route = Route::ranked(server.rank, Method::Get, "/<path..>", server);
189 route.name = Some(format!("FileServer: {}", source).into());
190 vec![route]
191 }
192}
193
194#[crate::async_trait]
195impl Handler for FileServer {
196 async fn handle<'r>(&self, req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r> {
197 use crate::http::uri::fmt::Path;
198
199 // TODO: Should we reject dotfiles for `self.root` if !DotFiles?
200 let options = self.options;
201 if options.contains(Options::IndexFile) && self.root.is_file() {
202 let segments = match req.segments::<Segments<'_, Path>>(0..) {
203 Ok(segments) => segments,
204 Err(never) => match never {},
205 };
206
207 if segments.is_empty() {
208 let file = NamedFile::open(&self.root).await;
209 return file.respond_to(req).or_forward((data, Status::NotFound));
210 } else {
211 return Outcome::forward(data, Status::NotFound);
212 }
213 }
214
215 // Get the segments as a `PathBuf`, allowing dotfiles requested.
216 let allow_dotfiles = options.contains(Options::DotFiles);
217 let path = req.segments::<Segments<'_, Path>>(0..).ok()
218 .and_then(|segments| segments.to_path_buf(allow_dotfiles).ok())
219 .map(|path| self.root.join(path));
220
221 match path {
222 Some(p) if p.is_dir() => {
223 // Normalize '/a/b/foo' to '/a/b/foo/'.
224 if options.contains(Options::NormalizeDirs) && !req.uri().path().ends_with('/') {
225 let normal = req.uri().map_path(|p| format!("{}/", p))
226 .expect("adding a trailing slash to a known good path => valid path")
227 .into_owned();
228
229 return Redirect::permanent(normal)
230 .respond_to(req)
231 .or_forward((data, Status::InternalServerError));
232 }
233
234 if !options.contains(Options::Index) {
235 return Outcome::forward(data, Status::NotFound);
236 }
237
238 let index = NamedFile::open(p.join("index.html")).await;
239 index.respond_to(req).or_forward((data, Status::NotFound))
240 },
241 Some(p) => {
242 let file = NamedFile::open(p).await;
243 file.respond_to(req).or_forward((data, Status::NotFound))
244 }
245 None => Outcome::forward(data, Status::NotFound),
246 }
247 }
248}
249
250/// A bitset representing configurable options for [`FileServer`].
251///
252/// The valid options are:
253///
254/// * [`Options::None`] - Return only present, visible files.
255/// * [`Options::DotFiles`] - In addition to visible files, return dotfiles.
256/// * [`Options::Index`] - Render `index.html` pages for directory requests.
257/// * [`Options::IndexFile`] - Allow serving a single file as the index.
258/// * [`Options::Missing`] - Don't fail if the path to serve is missing.
259/// * [`Options::NormalizeDirs`] - Redirect directories without a trailing
260/// slash to ones with a trailing slash.
261///
262/// `Options` structures can be `or`d together to select two or more options.
263/// For instance, to request that both dot files and index pages be returned,
264/// use `Options::DotFiles | Options::Index`.
265#[derive(Debug, Clone, Copy)]
266pub struct Options(u8);
267
268#[allow(non_upper_case_globals, non_snake_case)]
269impl Options {
270 /// All options disabled.
271 ///
272 /// This is different than [`Options::default()`](#impl-Default), which
273 /// enables `Options::Index`.
274 pub const None: Options = Options(0);
275
276 /// Respond to requests for a directory with the `index.html` file in that
277 /// directory, if it exists.
278 ///
279 /// When enabled, [`FileServer`] will respond to requests for a directory
280 /// `/foo` or `/foo/` with the file at `${root}/foo/index.html` if it
281 /// exists. When disabled, requests to directories will always forward.
282 ///
283 /// **Enabled by default.**
284 pub const Index: Options = Options(1 << 0);
285
286 /// Allow serving dotfiles.
287 ///
288 /// When enabled, [`FileServer`] will respond to requests for files or
289 /// directories beginning with `.`. When disabled, any dotfiles will be
290 /// treated as missing.
291 ///
292 /// **Disabled by default.**
293 pub const DotFiles: Options = Options(1 << 1);
294
295 /// Normalizes directory requests by redirecting requests to directory paths
296 /// without a trailing slash to ones with a trailing slash.
297 ///
298 /// When enabled, the [`FileServer`] handler will respond to requests for a
299 /// directory without a trailing `/` with a permanent redirect (308) to the
300 /// same path with a trailing `/`. This ensures relative URLs within any
301 /// document served from that directory will be interpreted relative to that
302 /// directory rather than its parent.
303 ///
304 /// **Disabled by default.**
305 ///
306 /// # Example
307 ///
308 /// Given the following directory structure...
309 ///
310 /// ```text
311 /// static/
312 /// └── foo/
313 /// ├── cat.jpeg
314 /// └── index.html
315 /// ```
316 ///
317 /// ...with `FileServer::from("static")`, both requests to `/foo` and
318 /// `/foo/` will serve `static/foo/index.html`. If `index.html` references
319 /// `cat.jpeg` as a relative URL, the browser will request `/cat.jpeg`
320 /// (`static/cat.jpeg`) when the request for `/foo` was handled and
321 /// `/foo/cat.jpeg` (`static/foo/cat.jpeg`) if `/foo/` was handled. As a
322 /// result, the request in the former case will fail. To avoid this,
323 /// `NormalizeDirs` will redirect requests to `/foo` to `/foo/` if the file
324 /// that would be served is a directory.
325 pub const NormalizeDirs: Options = Options(1 << 2);
326
327 /// Allow serving a file instead of a directory.
328 ///
329 /// By default, `FileServer` will error on construction if the path to serve
330 /// does not point to a directory. When this option is enabled, if a path to
331 /// a file is provided, `FileServer` will serve the file as the root of the
332 /// mount path.
333 ///
334 /// # Example
335 ///
336 /// If the file tree looks like:
337 ///
338 /// ```text
339 /// static/
340 /// └── cat.jpeg
341 /// ```
342 ///
343 /// Then `cat.jpeg` can be served at `/cat` with:
344 ///
345 /// ```rust,no_run
346 /// # #[macro_use] extern crate rocket;
347 /// use rocket::fs::{FileServer, Options};
348 ///
349 /// #[launch]
350 /// fn rocket() -> _ {
351 /// rocket::build()
352 /// .mount("/cat", FileServer::new("static/cat.jpeg", Options::IndexFile))
353 /// }
354 /// ```
355 pub const IndexFile: Options = Options(1 << 3);
356
357 /// Don't fail if the file or directory to serve is missing.
358 ///
359 /// By default, `FileServer` will error if the path to serve is missing to
360 /// prevent inevitable 404 errors. This option overrides that.
361 pub const Missing: Options = Options(1 << 4);
362
363 /// Returns `true` if `self` is a superset of `other`. In other words,
364 /// returns `true` if all of the options in `other` are also in `self`.
365 ///
366 /// # Example
367 ///
368 /// ```rust
369 /// use rocket::fs::Options;
370 ///
371 /// let index_request = Options::Index | Options::DotFiles;
372 /// assert!(index_request.contains(Options::Index));
373 /// assert!(index_request.contains(Options::DotFiles));
374 ///
375 /// let index_only = Options::Index;
376 /// assert!(index_only.contains(Options::Index));
377 /// assert!(!index_only.contains(Options::DotFiles));
378 ///
379 /// let dot_only = Options::DotFiles;
380 /// assert!(dot_only.contains(Options::DotFiles));
381 /// assert!(!dot_only.contains(Options::Index));
382 /// ```
383 #[inline]
384 pub fn contains(self, other: Options) -> bool {
385 (other.0 & self.0) == other.0
386 }
387}
388
389/// The default set of options: `Options::Index`.
390impl Default for Options {
391 fn default() -> Self {
392 Options::Index
393 }
394}
395
396impl std::ops::BitOr for Options {
397 type Output = Self;
398
399 #[inline(always)]
400 fn bitor(self, rhs: Self) -> Self {
401 Options(self.0 | rhs.0)
402 }
403}
404
405crate::export! {
406 /// Generates a crate-relative version of a path.
407 ///
408 /// This macro is primarily intended for use with [`FileServer`] to serve
409 /// files from a path relative to the crate root.
410 ///
411 /// The macro accepts one parameter, `$path`, an absolute or (preferably)
412 /// relative path. It returns a path as an `&'static str` prefixed with the
413 /// path to the crate root. Use `Path::new(relative!($path))` to retrieve an
414 /// `&'static Path`.
415 ///
416 /// # Example
417 ///
418 /// Serve files from the crate-relative `static/` directory:
419 ///
420 /// ```rust
421 /// # #[macro_use] extern crate rocket;
422 /// use rocket::fs::{FileServer, relative};
423 ///
424 /// #[launch]
425 /// fn rocket() -> _ {
426 /// rocket::build().mount("/", FileServer::from(relative!("static")))
427 /// }
428 /// ```
429 ///
430 /// Path equivalences:
431 ///
432 /// ```rust
433 /// use std::path::Path;
434 ///
435 /// use rocket::fs::relative;
436 ///
437 /// let manual = Path::new(env!("CARGO_MANIFEST_DIR")).join("static");
438 /// let automatic_1 = Path::new(relative!("static"));
439 /// let automatic_2 = Path::new(relative!("/static"));
440 /// assert_eq!(manual, automatic_1);
441 /// assert_eq!(automatic_1, automatic_2);
442 /// ```
443 ///
444 macro_rules! relative {
445 ($path:expr) => {
446 if cfg!(windows) {
447 concat!(env!("CARGO_MANIFEST_DIR"), "\\", $path)
448 } else {
449 concat!(env!("CARGO_MANIFEST_DIR"), "/", $path)
450 }
451 };
452 }
453}