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