1use std::{
2 collections::Bound,
3 fs::Metadata,
4 io::{Seek, SeekFrom},
5 path::Path,
6 str::FromStr,
7 time::{SystemTime, UNIX_EPOCH},
8};
9
10use bytes::Bytes;
11use headers::{
12 ContentRange, ETag, HeaderMapExt, IfMatch, IfModifiedSince, IfNoneMatch, IfUnmodifiedSince,
13 Range,
14};
15use http::{header, StatusCode};
16use httpdate::HttpDate;
17use mime::Mime;
18use tokio::{fs::File, io::AsyncReadExt};
19
20use crate::{
21 error::StaticFileError, Body, FromRequest, IntoResponse, Request, RequestBody, Response, Result,
22};
23
24#[derive(Debug)]
26pub enum StaticFileResponse {
27 Ok {
29 body: Body,
31 content_length: u64,
33 content_type: Option<String>,
35 etag: Option<String>,
37 last_modified: Option<String>,
39 content_range: Option<(std::ops::Range<u64>, u64)>,
41 },
42 NotModified,
44}
45
46impl StaticFileResponse {
47 pub fn with_content_type(mut self, ct: impl Into<String>) -> Self {
49 if let StaticFileResponse::Ok { content_type, .. } = &mut self {
50 *content_type = Some(ct.into());
51 }
52 self
53 }
54}
55
56impl IntoResponse for StaticFileResponse {
57 fn into_response(self) -> Response {
58 match self {
59 StaticFileResponse::Ok {
60 body,
61 content_length,
62 content_type,
63 etag,
64 last_modified,
65 content_range,
66 } => {
67 let mut builder = Response::builder()
68 .header(header::ACCEPT_RANGES, "bytes")
69 .header(header::CONTENT_LENGTH, content_length);
70
71 if let Some(content_type) = content_type {
72 builder = builder.content_type(content_type);
73 }
74 if let Some(etag) = etag {
75 builder = builder.header(header::ETAG, etag);
76 }
77 if let Some(last_modified) = last_modified {
78 builder = builder.header(header::LAST_MODIFIED, last_modified);
79 }
80
81 if let Some((range, size)) = content_range {
82 builder = builder
83 .status(StatusCode::PARTIAL_CONTENT)
84 .typed_header(ContentRange::bytes(range, size).unwrap());
85 }
86
87 builder.body(body)
88 }
89 StaticFileResponse::NotModified => StatusCode::NOT_MODIFIED.into(),
90 }
91 }
92}
93
94#[derive(Debug)]
96pub struct StaticFileRequest {
97 if_match: Option<IfMatch>,
98 if_unmodified_since: Option<IfUnmodifiedSince>,
99 if_none_match: Option<IfNoneMatch>,
100 if_modified_since: Option<IfModifiedSince>,
101 range: Option<Range>,
102}
103
104impl<'a> FromRequest<'a> for StaticFileRequest {
105 async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result<Self> {
106 Ok(Self {
107 if_match: req.headers().typed_get::<IfMatch>(),
108 if_unmodified_since: req.headers().typed_get::<IfUnmodifiedSince>(),
109 if_none_match: req.headers().typed_get::<IfNoneMatch>(),
110 if_modified_since: req.headers().typed_get::<IfModifiedSince>(),
111 range: req.headers().typed_get::<Range>(),
112 })
113 }
114}
115
116impl StaticFileRequest {
117 pub fn create_response_from_data(
122 self,
123 data: impl AsRef<[u8]>,
124 ) -> Result<StaticFileResponse, StaticFileError> {
125 let data = data.as_ref();
126
127 let mut content_length = data.len() as u64;
129 let mut content_range = None;
130
131 let body = if let Some((start, end)) = self
132 .range
133 .and_then(|range| range.satisfiable_ranges(data.len() as u64).next())
134 {
135 let start = match start {
136 Bound::Included(n) => n,
137 Bound::Excluded(n) => n + 1,
138 Bound::Unbounded => 0,
139 };
140 let end = match end {
141 Bound::Included(n) => n + 1,
142 Bound::Excluded(n) => n,
143 Bound::Unbounded => content_length,
144 };
145 if end < start || end > content_length {
146 return Err(StaticFileError::RangeNotSatisfiable {
147 size: content_length,
148 });
149 }
150
151 if start != 0 || end != content_length {
152 content_range = Some((start..end, content_length));
153 }
154
155 content_length = end - start;
156 Body::from_bytes(Bytes::copy_from_slice(
157 &data[start as usize..(start + content_length) as usize],
158 ))
159 } else {
160 Body::from_bytes(Bytes::copy_from_slice(data))
161 };
162
163 Ok(StaticFileResponse::Ok {
164 body,
165 content_length,
166 content_type: None,
167 etag: None,
168 last_modified: None,
169 content_range,
170 })
171 }
172
173 pub fn create_response(
178 self,
179 path: impl AsRef<Path>,
180 prefer_utf8: bool,
181 ) -> Result<StaticFileResponse, StaticFileError> {
182 let path = path.as_ref();
183 if !path.exists() || !path.is_file() {
184 return Err(StaticFileError::NotFound);
185 }
186 let guess = mime_guess::from_path(path);
187 let mut file = std::fs::File::open(path)?;
188 let metadata = file.metadata()?;
189
190 let mut content_length = metadata.len();
192
193 let content_type = guess.first().map(|mime| {
195 if prefer_utf8 {
196 equiv_utf8_text(mime).to_string()
197 } else {
198 mime.to_string()
199 }
200 });
201
202 let mut etag_str = String::new();
204 let mut last_modified_str = String::new();
205
206 if let Ok(modified) = metadata.modified() {
207 etag_str = etag(ino(&metadata), &modified, metadata.len());
208 let etag = ETag::from_str(&etag_str).unwrap();
209
210 if let Some(if_match) = self.if_match {
211 if !if_match.precondition_passes(&etag) {
212 return Err(StaticFileError::PreconditionFailed);
213 }
214 }
215
216 if let Some(if_unmodified_since) = self.if_unmodified_since {
217 if !if_unmodified_since.precondition_passes(modified) {
218 return Err(StaticFileError::PreconditionFailed);
219 }
220 }
221
222 if let Some(if_non_match) = self.if_none_match {
223 if !if_non_match.precondition_passes(&etag) {
224 return Ok(StaticFileResponse::NotModified);
225 }
226 } else if let Some(if_modified_since) = self.if_modified_since {
227 if !if_modified_since.is_modified(modified) {
228 return Ok(StaticFileResponse::NotModified);
229 }
230 }
231
232 last_modified_str = HttpDate::from(modified).to_string();
233 }
234
235 let mut content_range = None;
236
237 let body = if let Some((start, end)) = self
238 .range
239 .and_then(|range| range.satisfiable_ranges(metadata.len()).next())
240 {
241 let start = match start {
242 Bound::Included(n) => n,
243 Bound::Excluded(n) => n + 1,
244 Bound::Unbounded => 0,
245 };
246 let end = match end {
247 Bound::Included(n) => n + 1,
248 Bound::Excluded(n) => n,
249 Bound::Unbounded => metadata.len(),
250 };
251 if end < start || end > metadata.len() {
252 return Err(StaticFileError::RangeNotSatisfiable {
253 size: metadata.len(),
254 });
255 }
256
257 if start != 0 || end != metadata.len() {
258 content_range = Some((start..end, metadata.len()));
259 }
260
261 content_length = end - start;
262 file.seek(SeekFrom::Start(start))?;
263 Body::from_async_read(File::from_std(file).take(end - start))
264 } else {
265 Body::from_async_read(File::from_std(file))
266 };
267
268 Ok(StaticFileResponse::Ok {
269 body,
270 content_length,
271 content_type,
272 etag: if !etag_str.is_empty() {
273 Some(etag_str)
274 } else {
275 None
276 },
277 last_modified: if !last_modified_str.is_empty() {
278 Some(last_modified_str)
279 } else {
280 None
281 },
282 content_range,
283 })
284 }
285}
286
287fn equiv_utf8_text(ct: Mime) -> Mime {
288 if ct == mime::APPLICATION_JAVASCRIPT {
289 return mime::APPLICATION_JAVASCRIPT_UTF_8;
290 }
291
292 if ct == mime::TEXT_HTML {
293 return mime::TEXT_HTML_UTF_8;
294 }
295
296 if ct == mime::TEXT_CSS {
297 return mime::TEXT_CSS_UTF_8;
298 }
299
300 if ct == mime::TEXT_PLAIN {
301 return mime::TEXT_PLAIN_UTF_8;
302 }
303
304 if ct == mime::TEXT_CSV {
305 return mime::TEXT_CSV_UTF_8;
306 }
307
308 if ct == mime::TEXT_TAB_SEPARATED_VALUES {
309 return mime::TEXT_TAB_SEPARATED_VALUES_UTF_8;
310 }
311
312 ct
313}
314
315#[allow(unused_variables)]
316fn ino(md: &Metadata) -> u64 {
317 #[cfg(unix)]
318 {
319 std::os::unix::fs::MetadataExt::ino(md)
320 }
321 #[cfg(not(unix))]
322 {
323 0
324 }
325}
326
327fn etag(ino: u64, modified: &SystemTime, len: u64) -> String {
328 let dur = modified
329 .duration_since(UNIX_EPOCH)
330 .expect("modification time must be after epoch");
331
332 format!(
333 "\"{:x}:{:x}:{:x}:{:x}\"",
334 ino,
335 len,
336 dur.as_secs(),
337 dur.subsec_nanos()
338 )
339}
340
341#[cfg(test)]
342mod tests {
343 use std::{path::Path, time::Duration};
344
345 use super::*;
346
347 impl StaticFileResponse {
348 fn etag(&self) -> String {
349 match self {
350 StaticFileResponse::Ok { etag, .. } => etag.clone().unwrap(),
351 _ => panic!(),
352 }
353 }
354
355 fn last_modified(&self) -> String {
356 match self {
357 StaticFileResponse::Ok { last_modified, .. } => last_modified.clone().unwrap(),
358 _ => panic!(),
359 }
360 }
361 }
362
363 #[test]
364 fn test_equiv_utf8_text() {
365 assert_eq!(
366 equiv_utf8_text(mime::APPLICATION_JAVASCRIPT),
367 mime::APPLICATION_JAVASCRIPT_UTF_8
368 );
369 assert_eq!(equiv_utf8_text(mime::TEXT_HTML), mime::TEXT_HTML_UTF_8);
370 assert_eq!(equiv_utf8_text(mime::TEXT_CSS), mime::TEXT_CSS_UTF_8);
371 assert_eq!(equiv_utf8_text(mime::TEXT_PLAIN), mime::TEXT_PLAIN_UTF_8);
372 assert_eq!(equiv_utf8_text(mime::TEXT_CSV), mime::TEXT_CSV_UTF_8);
373 assert_eq!(
374 equiv_utf8_text(mime::TEXT_TAB_SEPARATED_VALUES),
375 mime::TEXT_TAB_SEPARATED_VALUES_UTF_8
376 );
377
378 assert_eq!(equiv_utf8_text(mime::TEXT_XML), mime::TEXT_XML);
379 assert_eq!(equiv_utf8_text(mime::IMAGE_PNG), mime::IMAGE_PNG);
380 }
381
382 async fn check_response(req: Request) -> Result<StaticFileResponse, StaticFileError> {
383 let static_file = StaticFileRequest::from_request_without_body(&req)
384 .await
385 .unwrap();
386 static_file.create_response(Path::new("Cargo.toml"), false)
387 }
388
389 #[tokio::test]
390 async fn test_if_none_match() {
391 let resp = check_response(Request::default()).await.unwrap();
392 assert!(matches!(resp, StaticFileResponse::Ok { .. }));
393 let etag = resp.etag();
394
395 let resp = check_response(Request::builder().header("if-none-match", etag).finish())
396 .await
397 .unwrap();
398 assert!(matches!(resp, StaticFileResponse::NotModified));
399
400 let resp = check_response(Request::builder().header("if-none-match", "abc").finish())
401 .await
402 .unwrap();
403 assert!(matches!(resp, StaticFileResponse::Ok { .. }));
404 }
405
406 #[tokio::test]
407 async fn test_if_modified_since() {
408 let resp = check_response(Request::default()).await.unwrap();
409 assert!(matches!(resp, StaticFileResponse::Ok { .. }));
410 let modified = resp.last_modified();
411
412 let resp = check_response(
413 Request::builder()
414 .header("if-modified-since", &modified)
415 .finish(),
416 )
417 .await
418 .unwrap();
419 assert!(matches!(resp, StaticFileResponse::NotModified));
420
421 let mut t: SystemTime = HttpDate::from_str(&modified).unwrap().into();
422 t -= Duration::from_secs(1);
423
424 let resp = check_response(
425 Request::builder()
426 .header("if-modified-since", HttpDate::from(t).to_string())
427 .finish(),
428 )
429 .await
430 .unwrap();
431 assert!(matches!(resp, StaticFileResponse::Ok { .. }));
432
433 let mut t: SystemTime = HttpDate::from_str(&modified).unwrap().into();
434 t += Duration::from_secs(1);
435
436 let resp = check_response(
437 Request::builder()
438 .header("if-modified-since", HttpDate::from(t).to_string())
439 .finish(),
440 )
441 .await
442 .unwrap();
443 assert!(matches!(resp, StaticFileResponse::NotModified));
444 }
445
446 #[tokio::test]
447 async fn test_if_match() {
448 let resp = check_response(Request::default()).await.unwrap();
449 assert!(matches!(resp, StaticFileResponse::Ok { .. }));
450 let etag = resp.etag();
451
452 let resp = check_response(Request::builder().header("if-match", etag).finish())
453 .await
454 .unwrap();
455 assert!(matches!(resp, StaticFileResponse::Ok { .. }));
456
457 let err = check_response(Request::builder().header("if-match", "abc").finish())
458 .await
459 .unwrap_err();
460 assert!(matches!(err, StaticFileError::PreconditionFailed));
461 }
462
463 #[tokio::test]
464 async fn test_if_unmodified_since() {
465 let resp = check_response(Request::default()).await.unwrap();
466 assert!(matches!(resp, StaticFileResponse::Ok { .. }));
467 let modified = resp.last_modified();
468
469 let resp = check_response(
470 Request::builder()
471 .header("if-unmodified-since", &modified)
472 .finish(),
473 )
474 .await
475 .unwrap();
476 assert!(matches!(resp, StaticFileResponse::Ok { .. }));
477
478 let mut t: SystemTime = HttpDate::from_str(&modified).unwrap().into();
479 t += Duration::from_secs(1);
480 let resp = check_response(
481 Request::builder()
482 .header("if-unmodified-since", HttpDate::from(t).to_string())
483 .finish(),
484 )
485 .await
486 .unwrap();
487 assert!(matches!(resp, StaticFileResponse::Ok { .. }));
488
489 let mut t: SystemTime = HttpDate::from_str(&modified).unwrap().into();
490 t -= Duration::from_secs(1);
491 let err = check_response(
492 Request::builder()
493 .header("if-unmodified-since", HttpDate::from(t).to_string())
494 .finish(),
495 )
496 .await
497 .unwrap_err();
498 assert!(matches!(err, StaticFileError::PreconditionFailed));
499 }
500
501 #[tokio::test]
502 async fn test_range_partial_content() {
503 let static_file = StaticFileRequest::from_request_without_body(
504 &Request::builder()
505 .typed_header(Range::bytes(0..10).unwrap())
506 .finish(),
507 )
508 .await
509 .unwrap();
510 let resp = static_file
511 .create_response(Path::new("Cargo.toml"), false)
512 .unwrap();
513 match resp {
514 StaticFileResponse::Ok { content_range, .. } => {
515 assert_eq!(content_range.unwrap().0, 0..10);
516 }
517 StaticFileResponse::NotModified => panic!(),
518 }
519 }
520
521 #[tokio::test]
522 async fn test_range_full_content() {
523 let md = std::fs::metadata("Cargo.toml").unwrap();
524
525 let static_file = StaticFileRequest::from_request_without_body(
526 &Request::builder()
527 .typed_header(Range::bytes(0..md.len()).unwrap())
528 .finish(),
529 )
530 .await
531 .unwrap();
532 let resp = static_file
533 .create_response(Path::new("Cargo.toml"), false)
534 .unwrap();
535 match resp {
536 StaticFileResponse::Ok { content_range, .. } => {
537 assert!(content_range.is_none());
538 }
539 StaticFileResponse::NotModified => panic!(),
540 }
541 }
542
543 #[tokio::test]
544 async fn test_range_413() {
545 let md = std::fs::metadata("Cargo.toml").unwrap();
546
547 let static_file = StaticFileRequest::from_request_without_body(
548 &Request::builder()
549 .typed_header(Range::bytes(0..md.len() + 1).unwrap())
550 .finish(),
551 )
552 .await
553 .unwrap();
554 let err = static_file
555 .create_response(Path::new("Cargo.toml"), false)
556 .unwrap_err();
557
558 match err {
559 StaticFileError::RangeNotSatisfiable { size } => assert_eq!(size, md.len()),
560 _ => panic!(),
561 }
562 }
563}