|
| 1 | +use core::fmt; |
1 | 2 | use std::path::{PathBuf, Path};
|
| 3 | +use std::sync::Arc; |
| 4 | +use std::time::SystemTime; |
2 | 5 |
|
3 |
| -use crate::{Request, Data}; |
4 |
| -use crate::http::{Method, Status, uri::Segments, ext::IntoOwned}; |
| 6 | + |
| 7 | +use crate::{Data, Request}; |
| 8 | +use crate::http::{Method, Status, uri::{Segments, Uri}, ext::IntoOwned, HeaderMap, Header}; |
5 | 9 | use crate::route::{Route, Handler, Outcome};
|
6 | 10 | use crate::response::{Redirect, Responder};
|
7 | 11 | use crate::outcome::IntoOutcome;
|
@@ -60,13 +64,118 @@ use crate::fs::NamedFile;
|
60 | 64 | /// rocket::build().mount("/", FileServer::from(relative!("static")))
|
61 | 65 | /// }
|
62 | 66 | /// ```
|
63 |
| -#[derive(Debug, Clone)] |
| 67 | +#[derive(Clone)] |
64 | 68 | pub struct FileServer {
|
65 | 69 | root: PathBuf,
|
66 | 70 | options: Options,
|
| 71 | + // TODO: I'd prefer box, but this just makes Clone easier. |
| 72 | + rewrites: Vec<Arc<dyn Rewrite>>, |
67 | 73 | rank: isize,
|
68 | 74 | }
|
69 | 75 |
|
| 76 | +impl fmt::Debug for FileServer { |
| 77 | + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 78 | + f.debug_struct("FileServer") |
| 79 | + .field("root", &self.root) |
| 80 | + .field("options", &self.options) |
| 81 | + .field("rewrites", &DebugListRewrite(&self.rewrites)) |
| 82 | + .field("rank", &self.rank) |
| 83 | + .finish() |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +struct DebugListRewrite<'a>(&'a Vec<Arc<dyn Rewrite>>); |
| 88 | + |
| 89 | +impl fmt::Debug for DebugListRewrite<'_> { |
| 90 | + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 91 | + f.debug_list().entries(self.0.iter().map(|r| r.name())).finish() |
| 92 | + } |
| 93 | +} |
| 94 | + |
| 95 | +pub trait Rewrite: Send + Sync + 'static { |
| 96 | + fn name(&self) -> &'static str { |
| 97 | + std::any::type_name::<Self>() |
| 98 | + } |
| 99 | + /// Modify RewritablePath as needed. |
| 100 | + fn rewrite(&self, req: &Request<'_>, path: FileServerResponse, root: &Path) -> FileServerResponse; |
| 101 | +} |
| 102 | + |
| 103 | +pub enum HiddenReason { |
| 104 | + DotFile, |
| 105 | + PermissionDenied, |
| 106 | + Other, |
| 107 | +} |
| 108 | + |
| 109 | +pub enum FileServerResponse { |
| 110 | + /// Status: Ok |
| 111 | + File { |
| 112 | + name: PathBuf, |
| 113 | + modified: Option<SystemTime>, |
| 114 | + headers: HeaderMap<'static>, |
| 115 | + }, |
| 116 | + /// Status: NotFound |
| 117 | + NotFound { name: PathBuf }, |
| 118 | + /// Status: NotFound (used to identify if the file does actually exist) |
| 119 | + Hidden { name: PathBuf, reason: HiddenReason }, |
| 120 | + /// Status: Redirect |
| 121 | + PermanentRedirect { to: Uri<'static> }, |
| 122 | + /// Status: Redirect (TODO: should we allow this?) |
| 123 | + TemporaryRedirect { to: Uri<'static> }, |
| 124 | +} |
| 125 | + |
| 126 | +// These might have to remain as basic options (always processed first) |
| 127 | +struct DotFiles; |
| 128 | +impl Rewrite for DotFiles { |
| 129 | + fn rewrite(&self, _req: &Request<'_>, path: FileServerResponse, _root: &Path) -> FileServerResponse { |
| 130 | + match path { |
| 131 | + FileServerResponse::Hidden { name, reason: HiddenReason::DotFile } => FileServerResponse::File { name, modified: None, headers: HeaderMap::new() }, |
| 132 | + path => path, |
| 133 | + } |
| 134 | + } |
| 135 | +} |
| 136 | +// struct Missing; // This only applies on startup, so this needs to be an option |
| 137 | +// impl Rewrite for Missing { |
| 138 | +// fn rewrite(&self, req: &Request<'_>, path: &mut RewritablePath<'_>) { |
| 139 | +// todo!() |
| 140 | +// } |
| 141 | +// } |
| 142 | + |
| 143 | +struct Index(&'static str); |
| 144 | +impl Rewrite for Index { |
| 145 | + fn rewrite(&self, _req: &Request<'_>, path: FileServerResponse, _root: &Path) -> FileServerResponse { |
| 146 | + // if path.file_name_path.is_dir() { |
| 147 | + // path.file_name_path.push(self.0); |
| 148 | + // // TODO: handle file_data_path |
| 149 | + // } |
| 150 | + match path { |
| 151 | + FileServerResponse::File { name, modified, headers } if name.is_dir() => FileServerResponse::File { name: name.join(self.0), modified, headers }, |
| 152 | + path => path, |
| 153 | + } |
| 154 | + } |
| 155 | +} |
| 156 | +// I'm not sure this one works, since we should check it during startup |
| 157 | +struct IndexFile; |
| 158 | +impl Rewrite for IndexFile { |
| 159 | + fn rewrite(&self, req: &Request<'_>, path: FileServerResponse, root: &Path) -> FileServerResponse { |
| 160 | + todo!() |
| 161 | + } |
| 162 | +} |
| 163 | + |
| 164 | +struct NormalizeDirs; |
| 165 | +impl Rewrite for NormalizeDirs { |
| 166 | + fn rewrite(&self, req: &Request<'_>, path: FileServerResponse, _root: &Path) -> FileServerResponse { |
| 167 | + match path { |
| 168 | + FileServerResponse::File { name, .. } if name.is_dir() && !req.uri().path().ends_with('/') => |
| 169 | + FileServerResponse::PermanentRedirect { |
| 170 | + to: req.uri().map_path(|p| format!("{}/", p)) |
| 171 | + .expect("adding a trailing slash to a known good path => valid path") |
| 172 | + .into_owned().into() |
| 173 | + }, |
| 174 | + path => path, |
| 175 | + } |
| 176 | + } |
| 177 | +} |
| 178 | + |
70 | 179 | impl FileServer {
|
71 | 180 | /// The default rank use by `FileServer` routes.
|
72 | 181 | const DEFAULT_RANK: isize = 10;
|
@@ -159,7 +268,12 @@ impl FileServer {
|
159 | 268 | }
|
160 | 269 | }
|
161 | 270 |
|
162 |
| - FileServer { root: path.into(), options, rank: Self::DEFAULT_RANK } |
| 271 | + FileServer { root: path.into(), options, rewrites: vec![], rank: Self::DEFAULT_RANK } |
| 272 | + } |
| 273 | + |
| 274 | + pub fn rewrite(mut self, rewrite: impl Rewrite) -> Self { |
| 275 | + self.rewrites.push(Arc::new(rewrite)); |
| 276 | + self |
163 | 277 | }
|
164 | 278 |
|
165 | 279 | /// Sets the rank for generated routes to `rank`.
|
@@ -193,55 +307,33 @@ impl From<FileServer> for Vec<Route> {
|
193 | 307 | #[crate::async_trait]
|
194 | 308 | impl Handler for FileServer {
|
195 | 309 | async fn handle<'r>(&self, req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r> {
|
196 |
| - use crate::http::uri::fmt::Path; |
197 |
| - |
198 |
| - // TODO: Should we reject dotfiles for `self.root` if !DotFiles? |
199 |
| - let options = self.options; |
200 |
| - if options.contains(Options::IndexFile) && self.root.is_file() { |
201 |
| - let segments = match req.segments::<Segments<'_, Path>>(0..) { |
202 |
| - Ok(segments) => segments, |
203 |
| - Err(never) => match never {}, |
204 |
| - }; |
205 |
| - |
206 |
| - if segments.is_empty() { |
207 |
| - let file = NamedFile::open(&self.root).await; |
208 |
| - return file.respond_to(req).or_forward((data, Status::NotFound)); |
209 |
| - } else { |
210 |
| - return Outcome::forward(data, Status::NotFound); |
211 |
| - } |
| 310 | + use crate::http::uri::fmt::Path as UriPath; |
| 311 | + // let options = self.options; |
| 312 | + let path = req.segments::<Segments<'_, UriPath>>(0..).ok() |
| 313 | + .and_then(|segments| segments.to_path_buf_dotfiles().ok()); |
| 314 | + // .map(|path| self.root.join(path)); |
| 315 | + let mut response = match path { |
| 316 | + Some((name, false)) => FileServerResponse::File { name, modified: None, headers: HeaderMap::new() }, |
| 317 | + Some((name, true)) => FileServerResponse::Hidden { name, reason: HiddenReason::DotFile }, |
| 318 | + None => return Outcome::forward(data, Status::NotFound), |
| 319 | + }; |
| 320 | + for rewrite in &self.rewrites { |
| 321 | + response = rewrite.rewrite(req, response, &self.root); |
212 | 322 | }
|
213 |
| - |
214 |
| - // Get the segments as a `PathBuf`, allowing dotfiles requested. |
215 |
| - let allow_dotfiles = options.contains(Options::DotFiles); |
216 |
| - let path = req.segments::<Segments<'_, Path>>(0..).ok() |
217 |
| - .and_then(|segments| segments.to_path_buf(allow_dotfiles).ok()) |
218 |
| - .map(|path| self.root.join(path)); |
219 |
| - |
220 |
| - match path { |
221 |
| - Some(p) if p.is_dir() => { |
222 |
| - // Normalize '/a/b/foo' to '/a/b/foo/'. |
223 |
| - if options.contains(Options::NormalizeDirs) && !req.uri().path().ends_with('/') { |
224 |
| - let normal = req.uri().map_path(|p| format!("{}/", p)) |
225 |
| - .expect("adding a trailing slash to a known good path => valid path") |
226 |
| - .into_owned(); |
227 |
| - |
228 |
| - return Redirect::permanent(normal) |
229 |
| - .respond_to(req) |
230 |
| - .or_forward((data, Status::InternalServerError)); |
| 323 | + match response { |
| 324 | + FileServerResponse::File { name, modified, headers } => NamedFile::open(self.root.join(name)).await.respond_to(req).map(|mut r| { |
| 325 | + for header in headers { |
| 326 | + r.adjoin_raw_header(header.name.as_str().to_owned(), header.value); |
231 | 327 | }
|
232 |
| - |
233 |
| - if !options.contains(Options::Index) { |
234 |
| - return Outcome::forward(data, Status::NotFound); |
| 328 | + if let Some(modified) = modified { |
| 329 | + // TODO: must be converted to http-data format |
| 330 | + // r.set_header(Header::new("Last-Modified", format!("{:?}", modified))); |
235 | 331 | }
|
236 |
| - |
237 |
| - let index = NamedFile::open(p.join("index.html")).await; |
238 |
| - index.respond_to(req).or_forward((data, Status::NotFound)) |
239 |
| - }, |
240 |
| - Some(p) => { |
241 |
| - let file = NamedFile::open(p).await; |
242 |
| - file.respond_to(req).or_forward((data, Status::NotFound)) |
243 |
| - } |
244 |
| - None => Outcome::forward(data, Status::NotFound), |
| 332 | + r |
| 333 | + }).or_forward((data, Status::NotFound)), |
| 334 | + FileServerResponse::Hidden { .. } | FileServerResponse::NotFound { ..} => Outcome::forward(data, Status::NotFound), |
| 335 | + FileServerResponse::PermanentRedirect { to } => Redirect::permanent(to).respond_to(req).or_forward((data, Status::InternalServerError)), |
| 336 | + FileServerResponse::TemporaryRedirect { to } => Redirect::temporary(to).respond_to(req).or_forward((data, Status::InternalServerError)), |
245 | 337 | }
|
246 | 338 | }
|
247 | 339 | }
|
|
0 commit comments