|
| 1 | +use std::borrow::Cow; |
| 2 | +use std::path::{Path, PathBuf}; |
| 3 | + |
| 4 | +use crate::Request; |
| 5 | +use crate::http::{ext::IntoOwned, HeaderMap}; |
| 6 | +use crate::response::Redirect; |
| 7 | + |
| 8 | +/// A file server [`Rewrite`] rewriter. |
| 9 | +/// |
| 10 | +/// A [`FileServer`] is a sequence of [`Rewriter`]s which transform the incoming |
| 11 | +/// request path into a [`Rewrite`] or `None`. The first rewriter is called with |
| 12 | +/// the request path as a [`Rewrite::File`]. Each `Rewriter` thereafter is |
| 13 | +/// called in-turn with the previously returned [`Rewrite`], and the value |
| 14 | +/// returned from the last `Rewriter` is used to respond to the request. If the |
| 15 | +/// final rewrite is `None` or a nonexistent path or a directory, [`FileServer`] |
| 16 | +/// responds with [`Status::NotFound`]. Otherwise it responds with the file |
| 17 | +/// contents, if [`Rewrite::File`] is specified, or a redirect, if |
| 18 | +/// [`Rewrite::Redirect`] is specified. |
| 19 | +/// |
| 20 | +/// [`FileServer`]: super::FileServer |
| 21 | +/// [`Status::NotFound`]: crate::http::Status::NotFound |
| 22 | +pub trait Rewriter: Send + Sync + 'static { |
| 23 | + /// Alter the [`Rewrite`] as needed. |
| 24 | + fn rewrite<'r>(&self, opt: Option<Rewrite<'r>>, req: &'r Request<'_>) -> Option<Rewrite<'r>>; |
| 25 | +} |
| 26 | + |
| 27 | +/// A Response from a [`FileServer`](super::FileServer) |
| 28 | +#[derive(Debug, Clone)] |
| 29 | +#[non_exhaustive] |
| 30 | +pub enum Rewrite<'r> { |
| 31 | + /// Return the contents of the specified file. |
| 32 | + File(File<'r>), |
| 33 | + /// Returns a Redirect. |
| 34 | + Redirect(Redirect), |
| 35 | +} |
| 36 | + |
| 37 | +/// A File response from a [`FileServer`](super::FileServer) and a rewriter. |
| 38 | +#[derive(Debug, Clone)] |
| 39 | +#[non_exhaustive] |
| 40 | +pub struct File<'r> { |
| 41 | + /// The path to the file that [`FileServer`](super::FileServer) will respond with. |
| 42 | + pub path: Cow<'r, Path>, |
| 43 | + /// A list of headers to be added to the generated response. |
| 44 | + pub headers: HeaderMap<'r>, |
| 45 | +} |
| 46 | + |
| 47 | +impl<'r> File<'r> { |
| 48 | + /// A new `File`, with not additional headers. |
| 49 | + pub fn new(path: impl Into<Cow<'r, Path>>) -> Self { |
| 50 | + Self { path: path.into(), headers: HeaderMap::new() } |
| 51 | + } |
| 52 | + |
| 53 | + /// A new `File`, with not additional headers. |
| 54 | + /// |
| 55 | + /// # Panics |
| 56 | + /// |
| 57 | + /// Panics if the `path` does not exist. |
| 58 | + pub fn checked<P: AsRef<Path>>(path: P) -> Self { |
| 59 | + let path = path.as_ref(); |
| 60 | + if !path.exists() { |
| 61 | + let path = path.display(); |
| 62 | + error!(%path, "FileServer path does not exist.\n\ |
| 63 | + Panicking to prevent inevitable handler error."); |
| 64 | + panic!("missing file {}: refusing to continue", path); |
| 65 | + } |
| 66 | + |
| 67 | + Self::new(path.to_path_buf()) |
| 68 | + } |
| 69 | + |
| 70 | + /// Replace the path in `self` with the result of applying `f` to the path. |
| 71 | + pub fn map_path<F, P>(self, f: F) -> Self |
| 72 | + where F: FnOnce(Cow<'r, Path>) -> P, P: Into<Cow<'r, Path>>, |
| 73 | + { |
| 74 | + Self { |
| 75 | + path: f(self.path).into(), |
| 76 | + headers: self.headers, |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + /// Returns `true` if the file is a dotfile. A dotfile is a file whose |
| 81 | + /// name or any directory in it's path start with a period (`.`) and is |
| 82 | + /// considered hidden. |
| 83 | + /// |
| 84 | + /// # Windows Note |
| 85 | + /// |
| 86 | + /// This does *not* check the file metadata on any platform, so hidden files |
| 87 | + /// on Windows will not be detected by this method. |
| 88 | + pub fn is_hidden(&self) -> bool { |
| 89 | + self.path.iter().any(|s| s.as_encoded_bytes().starts_with(b".")) |
| 90 | + } |
| 91 | + |
| 92 | + /// Returns `true` if the file is not hidden. This is the inverse of |
| 93 | + /// [`File::is_hidden()`]. |
| 94 | + pub fn is_visible(&self) -> bool { |
| 95 | + !self.is_hidden() |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +/// Prefixes all paths with a given path. |
| 100 | +/// |
| 101 | +/// # Example |
| 102 | +/// |
| 103 | +/// ```rust,no_run |
| 104 | +/// use rocket::fs::FileServer; |
| 105 | +/// use rocket::fs::rewrite::Prefix; |
| 106 | +/// |
| 107 | +/// FileServer::identity() |
| 108 | +/// .filter(|f, _| f.is_visible()) |
| 109 | +/// .rewrite(Prefix::checked("static")); |
| 110 | +/// ``` |
| 111 | +pub struct Prefix(PathBuf); |
| 112 | + |
| 113 | +impl Prefix { |
| 114 | + /// Panics if `path` does not exist. |
| 115 | + pub fn checked<P: AsRef<Path>>(path: P) -> Self { |
| 116 | + let path = path.as_ref(); |
| 117 | + if !path.is_dir() { |
| 118 | + let path = path.display(); |
| 119 | + error!(%path, "FileServer path is not a directory."); |
| 120 | + warn!("Aborting early to prevent inevitable handler error."); |
| 121 | + panic!("invalid directory: refusing to continue"); |
| 122 | + } |
| 123 | + |
| 124 | + Self(path.to_path_buf()) |
| 125 | + } |
| 126 | + |
| 127 | + /// Creates a new `Prefix` from a path. |
| 128 | + pub fn unchecked<P: AsRef<Path>>(path: P) -> Self { |
| 129 | + Self(path.as_ref().to_path_buf()) |
| 130 | + } |
| 131 | +} |
| 132 | + |
| 133 | +impl Rewriter for Prefix { |
| 134 | + fn rewrite<'r>(&self, opt: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> { |
| 135 | + opt.map(|r| match r { |
| 136 | + Rewrite::File(f) => Rewrite::File(f.map_path(|p| self.0.join(p))), |
| 137 | + Rewrite::Redirect(r) => Rewrite::Redirect(r), |
| 138 | + }) |
| 139 | + } |
| 140 | +} |
| 141 | + |
| 142 | +impl Rewriter for PathBuf { |
| 143 | + fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> { |
| 144 | + Some(Rewrite::File(File::new(self.clone()))) |
| 145 | + } |
| 146 | +} |
| 147 | + |
| 148 | +/// Normalize directories to always include a trailing slash by redirecting |
| 149 | +/// (with a 302 temporary redirect) requests for directories without a trailing |
| 150 | +/// slash to the same path with a trailing slash. |
| 151 | +/// |
| 152 | +/// # Example |
| 153 | +/// |
| 154 | +/// ```rust,no_run |
| 155 | +/// use rocket::fs::FileServer; |
| 156 | +/// use rocket::fs::rewrite::{Prefix, TrailingDirs}; |
| 157 | +/// |
| 158 | +/// FileServer::identity() |
| 159 | +/// .filter(|f, _| f.is_visible()) |
| 160 | +/// .rewrite(TrailingDirs); |
| 161 | +/// ``` |
| 162 | +pub struct TrailingDirs; |
| 163 | + |
| 164 | +impl Rewriter for TrailingDirs { |
| 165 | + fn rewrite<'r>(&self, opt: Option<Rewrite<'r>>, req: &Request<'_>) -> Option<Rewrite<'r>> { |
| 166 | + if let Some(Rewrite::File(f)) = &opt { |
| 167 | + if !req.uri().path().ends_with('/') && f.path.is_dir() { |
| 168 | + let uri = req.uri().clone().into_owned(); |
| 169 | + let uri = uri.map_path(|p| format!("{p}/")).unwrap(); |
| 170 | + return Some(Rewrite::Redirect(Redirect::temporary(uri))); |
| 171 | + } |
| 172 | + } |
| 173 | + |
| 174 | + opt |
| 175 | + } |
| 176 | +} |
| 177 | + |
| 178 | +/// Rewrite a directory to a file inside of that directory. |
| 179 | +/// |
| 180 | +/// # Example |
| 181 | +/// |
| 182 | +/// Rewrites all directory requests to `directory/index.html`. |
| 183 | +/// |
| 184 | +/// ```rust,no_run |
| 185 | +/// use rocket::fs::FileServer; |
| 186 | +/// use rocket::fs::rewrite::DirIndex; |
| 187 | +/// |
| 188 | +/// FileServer::without_index("static") |
| 189 | +/// .rewrite(DirIndex::if_exists("index.htm")) |
| 190 | +/// .rewrite(DirIndex::unconditional("index.html")); |
| 191 | +/// ``` |
| 192 | +pub struct DirIndex { |
| 193 | + path: PathBuf, |
| 194 | + check: bool, |
| 195 | +} |
| 196 | + |
| 197 | +impl DirIndex { |
| 198 | + /// Appends `path` to every request for a directory. |
| 199 | + pub fn unconditional(path: impl AsRef<Path>) -> Self { |
| 200 | + Self { path: path.as_ref().to_path_buf(), check: false } |
| 201 | + } |
| 202 | + |
| 203 | + /// Only appends `path` to a request for a directory if the file exists. |
| 204 | + pub fn if_exists(path: impl AsRef<Path>) -> Self { |
| 205 | + Self { path: path.as_ref().to_path_buf(), check: true } |
| 206 | + } |
| 207 | +} |
| 208 | + |
| 209 | +impl Rewriter for DirIndex { |
| 210 | + fn rewrite<'r>(&self, opt: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> { |
| 211 | + match opt? { |
| 212 | + Rewrite::File(f) if f.path.is_dir() => { |
| 213 | + let candidate = f.path.join(&self.path); |
| 214 | + if self.check && !candidate.is_file() { |
| 215 | + return Some(Rewrite::File(f)); |
| 216 | + } |
| 217 | + |
| 218 | + Some(Rewrite::File(f.map_path(|_| candidate))) |
| 219 | + } |
| 220 | + r => Some(r), |
| 221 | + } |
| 222 | + } |
| 223 | +} |
| 224 | + |
| 225 | +impl<'r> From<File<'r>> for Rewrite<'r> { |
| 226 | + fn from(value: File<'r>) -> Self { |
| 227 | + Self::File(value) |
| 228 | + } |
| 229 | +} |
| 230 | + |
| 231 | +impl<'r> From<Redirect> for Rewrite<'r> { |
| 232 | + fn from(value: Redirect) -> Self { |
| 233 | + Self::Redirect(value) |
| 234 | + } |
| 235 | +} |
| 236 | + |
| 237 | +impl<F: Send + Sync + 'static> Rewriter for F |
| 238 | + where F: for<'r> Fn(Option<Rewrite<'r>>, &Request<'_>) -> Option<Rewrite<'r>> |
| 239 | +{ |
| 240 | + fn rewrite<'r>(&self, f: Option<Rewrite<'r>>, r: &Request<'_>) -> Option<Rewrite<'r>> { |
| 241 | + self(f, r) |
| 242 | + } |
| 243 | +} |
| 244 | + |
| 245 | +impl Rewriter for Rewrite<'static> { |
| 246 | + fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> { |
| 247 | + Some(self.clone()) |
| 248 | + } |
| 249 | +} |
| 250 | + |
| 251 | +impl Rewriter for File<'static> { |
| 252 | + fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> { |
| 253 | + Some(Rewrite::File(self.clone())) |
| 254 | + } |
| 255 | +} |
| 256 | + |
| 257 | +impl Rewriter for Redirect { |
| 258 | + fn rewrite<'r>(&self, _: Option<Rewrite<'r>>, _: &Request<'_>) -> Option<Rewrite<'r>> { |
| 259 | + Some(Rewrite::Redirect(self.clone())) |
| 260 | + } |
| 261 | +} |
0 commit comments