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}