|
| 1 | +use std::borrow::Cow; |
| 2 | +use std::path::Path; |
| 3 | + |
| 4 | +use crate::{Request, Response}; |
| 5 | +use crate::http::{HeaderMap, ContentType}; |
| 6 | +use crate::http::ext::IntoOwned; |
| 7 | +use crate::response::{self, Redirect, Responder}; |
| 8 | + |
| 9 | +/// Trait used to implement [`FileServer`] customization. |
| 10 | +/// |
| 11 | +/// Conceptually, a [`FileServer`] is a sequence of `Rewriter`s, which transform |
| 12 | +/// a path from a request to a final response. [`FileServer`] add a set of default |
| 13 | +/// `Rewriter`s, which filter out dotfiles, apply a root path, normalize directories, |
| 14 | +/// and use `index.html`. |
| 15 | +/// |
| 16 | +/// After running the chain of `Rewriter`s, |
| 17 | +/// [`FileServer`] uses the final [`Option<Rewrite>`](Rewrite) |
| 18 | +/// to respond to the request. If the response is `None`, a path that doesn't |
| 19 | +/// exist or a directory path, [`FileServer`] will respond with a |
| 20 | +/// [`Status::NotFound`](crate::http::Status::NotFound). Otherwise the [`FileServer`] |
| 21 | +/// will respond with a redirect or the contents of the file specified. |
| 22 | +/// |
| 23 | +/// [`FileServer`] provides several helper methods to add `Rewriter`s: |
| 24 | +/// - [`FileServer::rewrite()`] |
| 25 | +/// - [`FileServer::filter()`] |
| 26 | +/// - [`FileServer::map()`] |
| 27 | +pub trait Rewriter: Send + Sync + 'static { |
| 28 | + /// Alter the [`Rewrite`] as needed. |
| 29 | + fn rewrite<'r>(&self, file: Option<Rewrite<'r>>, req: &'r Request<'_>) -> Option<Rewrite<'r>>; |
| 30 | +} |
| 31 | + |
| 32 | +/// A Response from a [`FileServer`] |
| 33 | +#[derive(Debug, Clone)] |
| 34 | +#[non_exhaustive] |
| 35 | +pub enum Rewrite<'r> { |
| 36 | + /// Return the contents of the specified file. |
| 37 | + File(File<'r>), |
| 38 | + /// Returns a Redirect. |
| 39 | + Redirect(Redirect), |
| 40 | +} |
| 41 | + |
| 42 | +/// A File response from a [`FileServer`] |
| 43 | +#[derive(Debug, Clone)] |
| 44 | +pub struct File<'r> { |
| 45 | + /// The path to the file that [`FileServer`] will respond with. |
| 46 | + pub path: Cow<'r, Path>, |
| 47 | + /// A list of headers to be added to the generated response. |
| 48 | + pub headers: HeaderMap<'r>, |
| 49 | +} |
| 50 | + |
| 51 | +impl<'r> Rewrite<'r> { |
| 52 | + pub fn file(&self) -> Option<&File<'r>> { |
| 53 | + match self { |
| 54 | + Rewrite::File(f) => Some(f), |
| 55 | + _ => None, |
| 56 | + } |
| 57 | + } |
| 58 | + |
| 59 | + pub fn redirect(&self) -> Option<&Redirect> { |
| 60 | + match self { |
| 61 | + Rewrite::Redirect(r) => Some(r), |
| 62 | + _ => None, |
| 63 | + } |
| 64 | + } |
| 65 | +} |
| 66 | + |
| 67 | +impl<'r> From<File<'r>> for Rewrite<'r> { |
| 68 | + fn from(value: File<'r>) -> Self { |
| 69 | + Self::File(value) |
| 70 | + } |
| 71 | +} |
| 72 | + |
| 73 | +impl<'r> From<Redirect> for Rewrite<'r> { |
| 74 | + fn from(value: Redirect) -> Self { |
| 75 | + Self::Redirect(value) |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +impl<'r> File<'r> { |
| 80 | + pub fn new(path: impl Into<Cow<'r, Path>>) -> Self { |
| 81 | + Self { path: path.into(), headers: HeaderMap::new() } |
| 82 | + } |
| 83 | + |
| 84 | + pub(crate) async fn open(self) -> std::io::Result<NamedFile<'r>> { |
| 85 | + let file = tokio::fs::File::open(&self.path).await?; |
| 86 | + let metadata = file.metadata().await?; |
| 87 | + if metadata.is_dir() { |
| 88 | + return Err(std::io::Error::new(std::io::ErrorKind::Other, "is a directory")); |
| 89 | + } |
| 90 | + |
| 91 | + Ok(NamedFile { |
| 92 | + file, |
| 93 | + len: metadata.len(), |
| 94 | + path: self.path, |
| 95 | + headers: self.headers, |
| 96 | + }) |
| 97 | + } |
| 98 | + |
| 99 | + /// Replace the path of this `File`. |
| 100 | + pub fn map_path<F, P>(self, f: F) -> Self |
| 101 | + where F: FnOnce(Cow<'r, Path>) -> P, |
| 102 | + P: Into<Cow<'r, Path>>, |
| 103 | + { |
| 104 | + Self { |
| 105 | + path: f(self.path).into(), |
| 106 | + headers: self.headers, |
| 107 | + } |
| 108 | + } |
| 109 | +} |
| 110 | + |
| 111 | +impl<F: Send + Sync + 'static> Rewriter for F |
| 112 | + where F: for<'r> Fn(Option<Rewrite<'r>>, &Request<'_>) -> Option<Rewrite<'r>> |
| 113 | +{ |
| 114 | + fn rewrite<'r>(&self, f: Option<Rewrite<'r>>, r: &Request<'_>) -> Option<Rewrite<'r>> { |
| 115 | + self(f, r) |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +impl Rewriter for Rewrite<'static> { |
| 120 | + fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> { |
| 121 | + Some(self.clone()) |
| 122 | + } |
| 123 | +} |
| 124 | + |
| 125 | +impl Rewriter for File<'static> { |
| 126 | + fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> { |
| 127 | + Some(Rewrite::File(self.clone())) |
| 128 | + } |
| 129 | +} |
| 130 | + |
| 131 | +impl Rewriter for Redirect { |
| 132 | + fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> { |
| 133 | + Some(Rewrite::Redirect(self.clone())) |
| 134 | + } |
| 135 | +} |
| 136 | + |
| 137 | +/// Helper trait to simplify standard rewrites |
| 138 | +#[doc(hidden)] |
| 139 | +pub trait FileMap: for<'r> Fn(File<'r>, &Request<'_>) -> Rewrite<'r> + Send + Sync + 'static {} |
| 140 | +impl<F> FileMap for F |
| 141 | + where F: for<'r> Fn(File<'r>, &Request<'_>) -> Rewrite<'r> + Send + Sync + 'static {} |
| 142 | + |
| 143 | +/// Helper trait to simplify standard rewrites |
| 144 | +#[doc(hidden)] |
| 145 | +pub trait FileFilter: Fn(&File<'_>, &Request<'_>) -> bool + Send + Sync + 'static {} |
| 146 | +impl<F> FileFilter for F |
| 147 | + where F: Fn(&File<'_>, &Request<'_>) -> bool + Send + Sync + 'static {} |
| 148 | + |
| 149 | +/// Prepends the provided path, to serve files from a directory. |
| 150 | +/// |
| 151 | +/// You can use [`relative!`] to make a path relative to the crate root, rather |
| 152 | +/// than the runtime directory. |
| 153 | +/// |
| 154 | +/// # Example |
| 155 | +/// |
| 156 | +/// ```rust,no_run |
| 157 | +/// # use rocket::fs::{FileServer, prefix, relative}; |
| 158 | +/// # fn make_server() -> FileServer { |
| 159 | +/// FileServer::empty() |
| 160 | +/// .map(prefix(relative!("static"))) |
| 161 | +/// # } |
| 162 | +/// ``` |
| 163 | +/// |
| 164 | +/// # Panics |
| 165 | +/// |
| 166 | +/// Panics if `path` does not exist. See [`file_root_permissive`] for a |
| 167 | +/// non-panicing variant. |
| 168 | +pub fn prefix(path: impl AsRef<Path>) -> impl FileMap { |
| 169 | + let path = path.as_ref(); |
| 170 | + if !path.is_dir() { |
| 171 | + let path = path.display(); |
| 172 | + error!(%path, "FileServer path is not a directory."); |
| 173 | + warn!("Aborting early to prevent inevitable handler error."); |
| 174 | + panic!("invalid directory: refusing to continue"); |
| 175 | + } |
| 176 | + |
| 177 | + let path = path.to_path_buf(); |
| 178 | + move |f, _r| { |
| 179 | + Rewrite::File(f.map_path(|p| path.join(p))) |
| 180 | + } |
| 181 | +} |
| 182 | + |
| 183 | +/// Prepends the provided path, to serve a single static file. |
| 184 | +/// |
| 185 | +/// # Example |
| 186 | +/// |
| 187 | +/// ```rust,no_run |
| 188 | +/// # use rocket::fs::{FileServer, file_root}; |
| 189 | +/// # fn make_server() -> FileServer { |
| 190 | +/// FileServer::empty() |
| 191 | +/// .map(file_root("static/index.html")) |
| 192 | +/// # } |
| 193 | +/// ``` |
| 194 | +/// |
| 195 | +/// # Panics |
| 196 | +/// |
| 197 | +/// Panics if `path` does not exist. See [`file_root_permissive`] for a |
| 198 | +/// non-panicing variant. |
| 199 | +pub fn file_root(path: impl AsRef<Path>) -> impl Rewriter { |
| 200 | + let path = path.as_ref(); |
| 201 | + if !path.exists() { |
| 202 | + let path = path.display(); |
| 203 | + error!(%path, "FileServer path does not exist."); |
| 204 | + warn!("Aborting early to prevent inevitable handler error."); |
| 205 | + panic!("invalid file: refusing to continue"); |
| 206 | + } |
| 207 | + |
| 208 | + Rewrite::File(File::new(path.to_path_buf())) |
| 209 | +} |
| 210 | + |
| 211 | +/// Rewrites the entire path with `path`. Does not check if `path` exists. |
| 212 | +/// |
| 213 | +/// # Example |
| 214 | +/// |
| 215 | +/// ```rust,no_run |
| 216 | +/// # use rocket::fs::{FileServer, file_root_permissive}; |
| 217 | +/// # fn make_server() -> FileServer { |
| 218 | +/// FileServer::empty() |
| 219 | +/// .map(file_root_permissive("/tmp/rocket")) |
| 220 | +/// # } |
| 221 | +/// ``` |
| 222 | +pub fn file_root_permissive(path: impl AsRef<Path>) -> impl Rewriter { |
| 223 | + let path = path.as_ref().to_path_buf(); |
| 224 | + Rewrite::File(File::new(path)) |
| 225 | +} |
| 226 | + |
| 227 | +/// Filters out any path that contains a file or directory name starting with a |
| 228 | +/// dot. If used after `prefix`, this will also check the root path for dots, and |
| 229 | +/// filter them. |
| 230 | +/// |
| 231 | +/// # Example |
| 232 | +/// |
| 233 | +/// ```rust,no_run |
| 234 | +/// # use rocket::fs::{FileServer, filter_dotfiles, prefix}; |
| 235 | +/// # fn make_server() -> FileServer { |
| 236 | +/// FileServer::empty() |
| 237 | +/// .filter(filter_dotfiles) |
| 238 | +/// .map(prefix("static")) |
| 239 | +/// # } |
| 240 | +/// ``` |
| 241 | +pub fn filter_dotfiles(file: &File<'_>, _req: &Request<'_>) -> bool { |
| 242 | + !file.path.iter().any(|s| s.as_encoded_bytes().starts_with(b".")) |
| 243 | +} |
| 244 | + |
| 245 | +/// Normalize directory accesses to always include a trailing slash. |
| 246 | +/// |
| 247 | +/// # Example |
| 248 | +/// |
| 249 | +/// Appends a slash to any request for a directory without a trailing slash |
| 250 | +/// ```rust,no_run |
| 251 | +/// # use rocket::fs::{FileServer, normalize_dirs, prefix}; |
| 252 | +/// # fn make_server() -> FileServer { |
| 253 | +/// FileServer::empty() |
| 254 | +/// .map(prefix("static")) |
| 255 | +/// .map(normalize_dirs) |
| 256 | +/// # } |
| 257 | +/// ``` |
| 258 | +pub fn normalize_dirs<'r>(file: File<'r>, req: &Request<'_>) -> Rewrite<'r> { |
| 259 | + if !req.uri().path().ends_with('/') && file.path.is_dir() { |
| 260 | + // Known good path + '/' is a good path. |
| 261 | + let uri = req.uri().clone().into_owned(); |
| 262 | + Rewrite::Redirect(Redirect::temporary(uri.map_path(|p| format!("{p}/")).unwrap())) |
| 263 | + } else { |
| 264 | + Rewrite::File(file) |
| 265 | + } |
| 266 | +} |
| 267 | + |
| 268 | +/// Unconditionally rewrite a directory to a file named `index` inside of that |
| 269 | +/// directory. |
| 270 | +/// |
| 271 | +/// # Example |
| 272 | +/// |
| 273 | +/// Rewrites all directory requests to `directory/index.html`. |
| 274 | +/// |
| 275 | +/// ```rust,no_run |
| 276 | +/// # use rocket::fs::{FileServer, index, prefix}; |
| 277 | +/// # fn make_server() -> FileServer { |
| 278 | +/// FileServer::empty() |
| 279 | +/// .map(prefix("static")) |
| 280 | +/// .map(index("index.html")) |
| 281 | +/// # } |
| 282 | +/// ``` |
| 283 | +pub fn index(index: &'static str) -> impl FileMap { |
| 284 | + move |f, _r| if f.path.is_dir() { |
| 285 | + Rewrite::File(f.map_path(|p| p.join(index))) |
| 286 | + } else { |
| 287 | + Rewrite::File(f) |
| 288 | + } |
| 289 | +} |
| 290 | + |
| 291 | +/// Rewrite a directory to a file named `index` inside of that directory if that |
| 292 | +/// file exists. Otherwise, leave the rewrite unchanged. |
| 293 | +pub fn try_index(index: &'static str) -> impl FileMap { |
| 294 | + move |f, _r| if f.path.is_dir() { |
| 295 | + let original_path = f.path.clone(); |
| 296 | + let index = original_path.join(index); |
| 297 | + if index.is_file() { |
| 298 | + Rewrite::File(f.map_path(|_| index)) |
| 299 | + } else { |
| 300 | + Rewrite::File(f) |
| 301 | + } |
| 302 | + } else { |
| 303 | + Rewrite::File(f) |
| 304 | + } |
| 305 | +} |
| 306 | + |
| 307 | +pub(crate) struct NamedFile<'r> { |
| 308 | + file: tokio::fs::File, |
| 309 | + len: u64, |
| 310 | + path: Cow<'r, Path>, |
| 311 | + headers: HeaderMap<'r>, |
| 312 | +} |
| 313 | + |
| 314 | +// Do we want to allow the user to rewrite the Content-Type? |
| 315 | +impl<'r> Responder<'r, 'r> for NamedFile<'r> { |
| 316 | + fn respond_to(self, _: &'r Request<'_>) -> response::Result<'r> { |
| 317 | + let mut response = Response::new(); |
| 318 | + response.set_header_map(self.headers); |
| 319 | + if !response.headers().contains("Content-Type") { |
| 320 | + self.path.extension() |
| 321 | + .and_then(|ext| ext.to_str()) |
| 322 | + .and_then(ContentType::from_extension) |
| 323 | + .map(|content_type| response.set_header(content_type)); |
| 324 | + } |
| 325 | + |
| 326 | + response.set_sized_body(self.len as usize, self.file); |
| 327 | + Ok(response) |
| 328 | + } |
| 329 | +} |
0 commit comments