1 //! Custom handler and options for static file serving.
2 //!
3 //! See the [`StaticFiles`] type for further details.
4 //!
5 //! # Enabling
6 //!
7 //! This module is only available when the `serve` feature is enabled. Enable it
8 //! in `Cargo.toml` as follows:
9 //!
10 //! ```toml
11 //! [dependencies.rocket_contrib]
12 //! version = "0.5.0-dev"
13 //! default-features = false
14 //! features = ["serve"]
15 //! ```
16 
17 use std::path::{PathBuf, Path};
18 
19 use rocket::{Request, Data, Route};
20 use rocket::http::{Method, uri::Segments, ext::IntoOwned};
21 use rocket::handler::{Handler, Outcome};
22 use rocket::response::{NamedFile, Redirect};
23 
24 /// A bitset representing configurable options for the [`StaticFiles`] handler.
25 ///
26 /// The valid options are:
27 ///
28 ///   * [`Options::None`] - Return only present, visible files.
29 ///   * [`Options::DotFiles`] - In addition to visible files, return dotfiles.
30 ///   * [`Options::Index`] - Render `index.html` pages for directory requests.
31 ///   * [`Options::NormalizeDirs`] - Redirect directories without a trailing
32 ///     slash to ones with a trailing slash.
33 ///
34 /// `Options` structures can be `or`d together to select two or more options.
35 /// For instance, to request that both dot files and index pages be returned,
36 /// use `Options::DotFiles | Options::Index`.
37 #[derive(Debug, Clone, Copy)]
38 pub struct Options(u8);
39 
40 #[allow(non_upper_case_globals, non_snake_case)]
41 impl Options {
42     /// `Options` representing the empty set. No dotfiles or index pages are
43     /// rendered. This is different than [`Options::default()`](#impl-Default),
44     /// which enables `Index`.
45     pub const None: Options = Options(0b0000);
46 
47     /// `Options` enabling responding to requests for a directory with the
48     /// `index.html` file in that directory, if it exists. When this is enabled,
49     /// the [`StaticFiles`] handler will respond to requests for a directory
50     /// `/foo` with the file `${root}/foo/index.html` if it exists. This is
51     /// enabled by default.
52     pub const Index: Options = Options(0b0001);
53 
54     /// `Options` enabling returning dot files. When this is enabled, the
55     /// [`StaticFiles`] handler will respond to requests for files or
56     /// directories beginning with `.`. This is _not_ enabled by default.
57     pub const DotFiles: Options = Options(0b0010);
58 
59     /// `Options` that normalizes directory requests by redirecting requests to
60     /// directory paths without a trailing slash to ones with a trailing slash.
61     ///
62     /// When enabled, the [`StaticFiles`] handler will respond to requests for a
63     /// directory without a trailing `/` with a permanent redirect (308) to the
64     /// same path with a trailing `/`. This ensures relative URLs within any
65     /// document served for that directory will be interpreted relative to that
66     /// directory, rather than its parent. This is _not_ enabled by default.
67     pub const NormalizeDirs: Options = Options(0b0100);
68 
69     /// Returns `true` if `self` is a superset of `other`. In other words,
70     /// returns `true` if all of the options in `other` are also in `self`.
71     ///
72     /// # Example
73     ///
74     /// ```rust
75     /// use rocket_contrib::serve::Options;
76     ///
77     /// let index_request = Options::Index | Options::DotFiles;
78     /// assert!(index_request.contains(Options::Index));
79     /// assert!(index_request.contains(Options::DotFiles));
80     ///
81     /// let index_only = Options::Index;
82     /// assert!(index_only.contains(Options::Index));
83     /// assert!(!index_only.contains(Options::DotFiles));
84     ///
85     /// let dot_only = Options::DotFiles;
86     /// assert!(dot_only.contains(Options::DotFiles));
87     /// assert!(!dot_only.contains(Options::Index));
88     /// ```
89     #[inline]
contains(self, other: Options) -> bool90     pub fn contains(self, other: Options) -> bool {
91         (other.0 & self.0) == other.0
92     }
93 }
94 
95 /// The default set of options: `Options::Index`.
96 impl Default for Options {
default() -> Self97     fn default() -> Self {
98         Options::Index
99     }
100 }
101 
102 impl std::ops::BitOr for Options {
103     type Output = Self;
104 
105     #[inline(always)]
bitor(self, rhs: Self) -> Self106     fn bitor(self, rhs: Self) -> Self {
107         Options(self.0 | rhs.0)
108     }
109 }
110 
111 /// Custom handler for serving static files.
112 ///
113 /// This handler makes it simple to serve static files from a directory on the
114 /// local file system. To use it, construct a `StaticFiles` using either
115 /// [`StaticFiles::from()`] or [`StaticFiles::new()`] then simply `mount` the
116 /// handler at a desired path. When mounted, the handler will generate route(s)
117 /// that serve the desired static files. If a requested file is not found, the
118 /// routes _forward_ the incoming request. The default rank of the generated
119 /// routes is `10`. To customize route ranking, use the [`StaticFiles::rank()`]
120 /// method.
121 ///
122 /// # Options
123 ///
124 /// The handler's functionality can be customized by passing an [`Options`] to
125 /// [`StaticFiles::new()`].
126 ///
127 /// # Example
128 ///
129 /// To serve files from the `/static` local file system directory at the
130 /// `/public` path, allowing `index.html` files to be used to respond to
131 /// requests for a directory (the default), you might write the following:
132 ///
133 /// ```rust
134 /// # extern crate rocket;
135 /// # extern crate rocket_contrib;
136 /// use rocket_contrib::serve::StaticFiles;
137 ///
138 /// fn main() {
139 /// # if false {
140 ///     rocket::ignite()
141 ///         .mount("/public", StaticFiles::from("/static"))
142 ///         .launch();
143 /// # }
144 /// }
145 /// ```
146 ///
147 /// With this, requests for files at `/public/<path..>` will be handled by
148 /// returning the contents of `./static/<path..>`. Requests for _directories_ at
149 /// `/public/<directory>` will be handled by returning the contents of
150 /// `./static/<directory>/index.html`.
151 ///
152 /// If your static files are stored relative to your crate and your project is
153 /// managed by Cargo, you should either use a relative path and ensure that your
154 /// server is started in the crate's root directory or use the
155 /// `CARGO_MANIFEST_DIR` to create an absolute path relative to your crate root.
156 /// For example, to serve files in the `static` subdirectory of your crate at
157 /// `/`, you might write:
158 ///
159 /// ```rust
160 /// # extern crate rocket;
161 /// # extern crate rocket_contrib;
162 /// use rocket_contrib::serve::StaticFiles;
163 ///
164 /// fn main() {
165 /// # if false {
166 ///     rocket::ignite()
167 ///         .mount("/", StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/static")))
168 ///         .launch();
169 /// # }
170 /// }
171 /// ```
172 #[derive(Clone)]
173 pub struct StaticFiles {
174     root: PathBuf,
175     options: Options,
176     rank: isize,
177 }
178 
179 impl StaticFiles {
180     /// The default rank use by `StaticFiles` routes.
181     const DEFAULT_RANK: isize = 10;
182 
183     /// Constructs a new `StaticFiles` that serves files from the file system
184     /// `path`. By default, [`Options::Index`] is set, and the generated routes
185     /// have a rank of `10`. To serve static files with other options, use
186     /// [`StaticFiles::new()`]. To choose a different rank for generated routes,
187     /// use [`StaticFiles::rank()`].
188     ///
189     /// # Example
190     ///
191     /// Serve the static files in the `/www/public` local directory on path
192     /// `/static`.
193     ///
194     /// ```rust
195     /// # extern crate rocket;
196     /// # extern crate rocket_contrib;
197     /// use rocket_contrib::serve::StaticFiles;
198     ///
199     /// fn main() {
200     /// # if false {
201     ///     rocket::ignite()
202     ///         .mount("/static", StaticFiles::from("/www/public"))
203     ///         .launch();
204     /// # }
205     /// }
206     /// ```
207     ///
208     /// Exactly as before, but set the rank for generated routes to `30`.
209     ///
210     /// ```rust
211     /// # extern crate rocket;
212     /// # extern crate rocket_contrib;
213     /// use rocket_contrib::serve::StaticFiles;
214     ///
215     /// fn main() {
216     /// # if false {
217     ///     rocket::ignite()
218     ///         .mount("/static", StaticFiles::from("/www/public").rank(30))
219     ///         .launch();
220     /// # }
221     /// }
222     /// ```
from<P: AsRef<Path>>(path: P) -> Self223     pub fn from<P: AsRef<Path>>(path: P) -> Self {
224         StaticFiles::new(path, Options::default())
225     }
226 
227     /// Constructs a new `StaticFiles` that serves files from the file system
228     /// `path` with `options` enabled. By default, the handler's routes have a
229     /// rank of `10`. To choose a different rank, use [`StaticFiles::rank()`].
230     ///
231     /// # Example
232     ///
233     /// Serve the static files in the `/www/public` local directory on path
234     /// `/static` without serving index files or dot files. Additionally, serve
235     /// the same files on `/pub` with a route rank of -1 while also serving
236     /// index files and dot files.
237     ///
238     /// ```rust
239     /// # extern crate rocket;
240     /// # extern crate rocket_contrib;
241     /// use rocket_contrib::serve::{StaticFiles, Options};
242     ///
243     /// fn main() {
244     /// # if false {
245     ///     let options = Options::Index | Options::DotFiles;
246     ///     rocket::ignite()
247     ///         .mount("/static", StaticFiles::from("/www/public"))
248     ///         .mount("/pub", StaticFiles::new("/www/public", options).rank(-1))
249     ///         .launch();
250     /// # }
251     /// }
252     /// ```
new<P: AsRef<Path>>(path: P, options: Options) -> Self253     pub fn new<P: AsRef<Path>>(path: P, options: Options) -> Self {
254         StaticFiles { root: path.as_ref().into(), options, rank: Self::DEFAULT_RANK }
255     }
256 
257     /// Sets the rank for generated routes to `rank`.
258     ///
259     /// # Example
260     ///
261     /// ```rust
262     /// # extern crate rocket_contrib;
263     /// use rocket_contrib::serve::{StaticFiles, Options};
264     ///
265     /// // A `StaticFiles` created with `from()` with routes of rank `3`.
266     /// StaticFiles::from("/public").rank(3);
267     ///
268     /// // A `StaticFiles` created with `new()` with routes of rank `-15`.
269     /// StaticFiles::new("/public", Options::Index).rank(-15);
270     /// ```
rank(mut self, rank: isize) -> Self271     pub fn rank(mut self, rank: isize) -> Self {
272         self.rank = rank;
273         self
274     }
275 }
276 
277 impl Into<Vec<Route>> for StaticFiles {
into(self) -> Vec<Route>278     fn into(self) -> Vec<Route> {
279         let non_index = Route::ranked(self.rank, Method::Get, "/<path..>", self.clone());
280         // `Index` requires routing the index for obvious reasons.
281         // `NormalizeDirs` requires routing the index so a `.mount("/foo")` with
282         // a request `/foo`, can be redirected to `/foo/`.
283         if self.options.contains(Options::Index) || self.options.contains(Options::NormalizeDirs) {
284             let index = Route::ranked(self.rank, Method::Get, "/", self);
285             vec![index, non_index]
286         } else {
287             vec![non_index]
288         }
289     }
290 }
291 
292 impl Handler for StaticFiles {
handle<'r>(&self, req: &'r Request<'_>, data: Data) -> Outcome<'r>293     fn handle<'r>(&self, req: &'r Request<'_>, data: Data) -> Outcome<'r> {
294         fn handle_dir<'r>(opt: Options, r: &'r Request<'_>, d: Data, path: &Path) -> Outcome<'r> {
295             if opt.contains(Options::NormalizeDirs) && !r.uri().path().ends_with('/') {
296                 let new_path = r.uri().map_path(|p| p.to_owned() + "/")
297                     .expect("adding a trailing slash to a known good path results in a valid path")
298                     .into_owned();
299 
300                 return Outcome::from_or_forward(r, d, Redirect::permanent(new_path));
301             }
302 
303             if !opt.contains(Options::Index) {
304                 return Outcome::forward(d);
305             }
306 
307             let file = NamedFile::open(path.join("index.html")).ok();
308             Outcome::from_or_forward(r, d, file)
309         }
310 
311         // If this is not the route with segments, handle it only if the user
312         // requested a handling of index files.
313         let current_route = req.route().expect("route while handling");
314         let is_segments_route = current_route.uri.path().ends_with(">");
315         if !is_segments_route {
316             return handle_dir(self.options, req, data, &self.root);
317         }
318 
319         // Otherwise, we're handling segments. Get the segments as a `PathBuf`,
320         // only allowing dotfiles if the user allowed it.
321         let allow_dotfiles = self.options.contains(Options::DotFiles);
322         let path = req.get_segments::<Segments<'_>>(0)
323             .and_then(|res| res.ok())
324             .and_then(|segments| segments.into_path_buf(allow_dotfiles).ok())
325             .map(|path| self.root.join(path));
326 
327         match &path {
328             Some(path) if path.is_dir() => handle_dir(self.options, req, data, path),
329             Some(path) => Outcome::from_or_forward(req, data, NamedFile::open(path).ok()),
330             None => Outcome::forward(data)
331         }
332     }
333 }
334