Skip to content

Commit 54b80b2

Browse files
committed
changes and improvements to rewrite PR
1 parent 14a1a27 commit 54b80b2

File tree

8 files changed

+496
-500
lines changed

8 files changed

+496
-500
lines changed

core/lib/src/fs/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ mod named_file;
55
mod temp_file;
66
mod file_name;
77

8+
pub mod rewrite;
9+
810
pub use server::*;
911
pub use named_file::*;
1012
pub use temp_file::*;

core/lib/src/fs/rewrite.rs

+329
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
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

Comments
 (0)