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