tower_http/services/fs/serve_dir/
open_file.rs1use super::{
2 headers::{IfModifiedSince, IfUnmodifiedSince, LastModified},
3 ServeVariant,
4};
5use crate::content_encoding::{Encoding, QValue};
6use bytes::Bytes;
7use http::{header, HeaderValue, Method, Request, Uri};
8use http_body_util::Empty;
9use http_range_header::RangeUnsatisfiableError;
10use std::{
11 ffi::OsStr,
12 fs::Metadata,
13 io::{self, SeekFrom},
14 ops::RangeInclusive,
15 path::{Path, PathBuf},
16};
17use tokio::{fs::File, io::AsyncSeekExt};
18
19pub(super) enum OpenFileOutput {
20 FileOpened(Box<FileOpened>),
21 Redirect { location: HeaderValue },
22 FileNotFound,
23 PreconditionFailed,
24 NotModified,
25}
26
27pub(super) struct FileOpened {
28 pub(super) extent: FileRequestExtent,
29 pub(super) chunk_size: usize,
30 pub(super) mime_header_value: HeaderValue,
31 pub(super) maybe_encoding: Option<Encoding>,
32 pub(super) maybe_range: Option<Result<Vec<RangeInclusive<u64>>, RangeUnsatisfiableError>>,
33 pub(super) last_modified: Option<LastModified>,
34}
35
36pub(super) enum FileRequestExtent {
37 Full(File, Metadata),
38 Head(Metadata),
39}
40
41pub(super) async fn open_file(
42 variant: ServeVariant,
43 mut path_to_file: PathBuf,
44 req: Request<Empty<Bytes>>,
45 negotiated_encodings: Vec<(Encoding, QValue)>,
46 range_header: Option<String>,
47 buf_chunk_size: usize,
48) -> io::Result<OpenFileOutput> {
49 let if_unmodified_since = req
50 .headers()
51 .get(header::IF_UNMODIFIED_SINCE)
52 .and_then(IfUnmodifiedSince::from_header_value);
53
54 let if_modified_since = req
55 .headers()
56 .get(header::IF_MODIFIED_SINCE)
57 .and_then(IfModifiedSince::from_header_value);
58
59 let mime = match variant {
60 ServeVariant::Directory {
61 append_index_html_on_directories,
62 } => {
63 if let Some(output) = maybe_redirect_or_append_path(
67 &mut path_to_file,
68 req.uri(),
69 append_index_html_on_directories,
70 )
71 .await
72 {
73 return Ok(output);
74 }
75
76 mime_guess::from_path(&path_to_file)
77 .first_raw()
78 .map(HeaderValue::from_static)
79 .unwrap_or_else(|| {
80 HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap()
81 })
82 }
83
84 ServeVariant::SingleFile { mime } => mime,
85 };
86
87 if req.method() == Method::HEAD {
88 let (meta, maybe_encoding) =
89 file_metadata_with_fallback(path_to_file, negotiated_encodings).await?;
90
91 let last_modified = meta.modified().ok().map(LastModified::from);
92 if let Some(output) = check_modified_headers(
93 last_modified.as_ref(),
94 if_unmodified_since,
95 if_modified_since,
96 ) {
97 return Ok(output);
98 }
99
100 let maybe_range = try_parse_range(range_header.as_deref(), meta.len());
101
102 Ok(OpenFileOutput::FileOpened(Box::new(FileOpened {
103 extent: FileRequestExtent::Head(meta),
104 chunk_size: buf_chunk_size,
105 mime_header_value: mime,
106 maybe_encoding,
107 maybe_range,
108 last_modified,
109 })))
110 } else {
111 let (mut file, maybe_encoding) =
112 open_file_with_fallback(path_to_file, negotiated_encodings).await?;
113 let meta = file.metadata().await?;
114 let last_modified = meta.modified().ok().map(LastModified::from);
115 if let Some(output) = check_modified_headers(
116 last_modified.as_ref(),
117 if_unmodified_since,
118 if_modified_since,
119 ) {
120 return Ok(output);
121 }
122
123 let maybe_range = try_parse_range(range_header.as_deref(), meta.len());
124 if let Some(Ok(ranges)) = maybe_range.as_ref() {
125 if ranges.len() == 1 {
128 file.seek(SeekFrom::Start(*ranges[0].start())).await?;
129 }
130 }
131
132 Ok(OpenFileOutput::FileOpened(Box::new(FileOpened {
133 extent: FileRequestExtent::Full(file, meta),
134 chunk_size: buf_chunk_size,
135 mime_header_value: mime,
136 maybe_encoding,
137 maybe_range,
138 last_modified,
139 })))
140 }
141}
142
143fn check_modified_headers(
144 modified: Option<&LastModified>,
145 if_unmodified_since: Option<IfUnmodifiedSince>,
146 if_modified_since: Option<IfModifiedSince>,
147) -> Option<OpenFileOutput> {
148 if let Some(since) = if_unmodified_since {
149 let precondition = modified
150 .as_ref()
151 .map(|time| since.precondition_passes(time))
152 .unwrap_or(false);
153
154 if !precondition {
155 return Some(OpenFileOutput::PreconditionFailed);
156 }
157 }
158
159 if let Some(since) = if_modified_since {
160 let unmodified = modified
161 .as_ref()
162 .map(|time| !since.is_modified(time))
163 .unwrap_or(false);
165 if unmodified {
166 return Some(OpenFileOutput::NotModified);
167 }
168 }
169
170 None
171}
172
173fn preferred_encoding(
176 path: &mut PathBuf,
177 negotiated_encoding: &[(Encoding, QValue)],
178) -> Option<Encoding> {
179 let preferred_encoding = Encoding::preferred_encoding(negotiated_encoding.iter().copied());
180
181 if let Some(file_extension) =
182 preferred_encoding.and_then(|encoding| encoding.to_file_extension())
183 {
184 let new_extension = path
185 .extension()
186 .map(|extension| {
187 let mut os_string = extension.to_os_string();
188 os_string.push(file_extension);
189 os_string
190 })
191 .unwrap_or_else(|| file_extension.to_os_string());
192
193 path.set_extension(new_extension);
194 }
195
196 preferred_encoding
197}
198
199async fn open_file_with_fallback(
203 mut path: PathBuf,
204 mut negotiated_encoding: Vec<(Encoding, QValue)>,
205) -> io::Result<(File, Option<Encoding>)> {
206 let (file, encoding) = loop {
207 let encoding = preferred_encoding(&mut path, &negotiated_encoding);
209 match (File::open(&path).await, encoding) {
210 (Ok(file), maybe_encoding) => break (file, maybe_encoding),
211 (Err(err), Some(encoding)) if err.kind() == io::ErrorKind::NotFound => {
212 path.set_extension(OsStr::new(""));
215 negotiated_encoding
217 .retain(|(negotiated_encoding, _)| *negotiated_encoding != encoding);
218 continue;
219 }
220 (Err(err), _) => return Err(err),
221 };
222 };
223 Ok((file, encoding))
224}
225
226async fn file_metadata_with_fallback(
230 mut path: PathBuf,
231 mut negotiated_encoding: Vec<(Encoding, QValue)>,
232) -> io::Result<(Metadata, Option<Encoding>)> {
233 let (file, encoding) = loop {
234 let encoding = preferred_encoding(&mut path, &negotiated_encoding);
236 match (tokio::fs::metadata(&path).await, encoding) {
237 (Ok(file), maybe_encoding) => break (file, maybe_encoding),
238 (Err(err), Some(encoding)) if err.kind() == io::ErrorKind::NotFound => {
239 path.set_extension(OsStr::new(""));
242 negotiated_encoding
244 .retain(|(negotiated_encoding, _)| *negotiated_encoding != encoding);
245 continue;
246 }
247 (Err(err), _) => return Err(err),
248 };
249 };
250 Ok((file, encoding))
251}
252
253async fn maybe_redirect_or_append_path(
254 path_to_file: &mut PathBuf,
255 uri: &Uri,
256 append_index_html_on_directories: bool,
257) -> Option<OpenFileOutput> {
258 if !is_dir(path_to_file).await {
259 return None;
260 }
261
262 if !append_index_html_on_directories {
263 return Some(OpenFileOutput::FileNotFound);
264 }
265
266 if uri.path().ends_with('/') {
267 path_to_file.push("index.html");
268 None
269 } else {
270 let location =
271 HeaderValue::from_str(&append_slash_on_path(uri.clone()).to_string()).unwrap();
272 Some(OpenFileOutput::Redirect { location })
273 }
274}
275
276fn try_parse_range(
277 maybe_range_ref: Option<&str>,
278 file_size: u64,
279) -> Option<Result<Vec<RangeInclusive<u64>>, RangeUnsatisfiableError>> {
280 maybe_range_ref.map(|header_value| {
281 http_range_header::parse_range_header(header_value)
282 .and_then(|first_pass| first_pass.validate(file_size))
283 })
284}
285
286async fn is_dir(path_to_file: &Path) -> bool {
287 tokio::fs::metadata(path_to_file)
288 .await
289 .map_or(false, |meta_data| meta_data.is_dir())
290}
291
292fn append_slash_on_path(uri: Uri) -> Uri {
293 let http::uri::Parts {
294 scheme,
295 authority,
296 path_and_query,
297 ..
298 } = uri.into_parts();
299
300 let mut uri_builder = Uri::builder();
301
302 if let Some(scheme) = scheme {
303 uri_builder = uri_builder.scheme(scheme);
304 }
305
306 if let Some(authority) = authority {
307 uri_builder = uri_builder.authority(authority);
308 }
309
310 let uri_builder = if let Some(path_and_query) = path_and_query {
311 if let Some(query) = path_and_query.query() {
312 uri_builder.path_and_query(format!("{}/?{}", path_and_query.path(), query))
313 } else {
314 uri_builder.path_and_query(format!("{}/", path_and_query.path()))
315 }
316 } else {
317 uri_builder.path_and_query("/")
318 };
319
320 uri_builder.build().unwrap()
321}