1use std::io::SeekFrom;
2use std::path::Path;
3use std::ffi::OsStr;
4use std::collections::HashMap;
5use std::sync::LazyLock;
6use std::time::UNIX_EPOCH;
7
8use tokio::io::AsyncSeekExt;
9use tokio::fs::File;
10
11use crate::core::HttpResult;
12use crate::reqres::{HttpRequest, HttpResponse, HttpHeader, HttpBody, StatusCode};
13use crate::util::httpdate;
14
15pub async fn file(req: &HttpRequest, name: &Path) -> HttpResult {
17 let mut file = File::open(name).await?;
18 let metadata = file.metadata().await?;
19 let mut len = metadata.len();
20
21 let mut code = StatusCode::OK;
23 let content_type = get_content_type(name.extension()).unwrap_or_default().to_string();
24 let mut headers = vec![];
25 let mut body;
26
27 let time = metadata.modified().ok();
29 if let Some(value) = time .and_then(httpdate::from_systime) {
32 headers.push(HttpHeader { name: "Last-Modified".to_string(), value });
33 }
34
35 if let Some(date) = httpdate::now() {
37 headers.push(HttpHeader { name: "Date".to_string(), value: date });
38 }
39
40 headers.push(HttpHeader {
42 name: "Accept-Ranges".to_string(),
43 value: "bytes".to_string(),
44 });
45
46 if let Some(range) = req.get_header("Range") {
48 if let Some((start, mut end)) = parse_range(range) && start <= len && start <= end {
49 end = end.min(len);
50
51 headers.push(HttpHeader {
52 name: "Content-Range".to_string(),
53 value: format!("bytes {start}-{end}/{len}"),
54 });
55
56 file.seek(SeekFrom::Start(start)).await?;
57 len = end - start + 1;
58 code = StatusCode::PARTIAL_CONTENT;
59 } else {
60 return Err(StatusCode::RANGE_NOT_SATISFIABLE.into());
62 }
63 }
64
65 body = HttpBody::File { file, len };
66
67 if let Some(time) = time
69 && let Some(time) = time.duration_since(UNIX_EPOCH).ok()
70 && let Some(if_modified_since) = req.get_header("If-Modified-Since")
71 && let Some(parsed) = httpdate::parse(if_modified_since)
72 && parsed >= time.as_secs() as i64
73 {
74 code = StatusCode::NOT_MODIFIED;
75 body = HttpBody::Empty;
76 }
77
78 Ok(HttpResponse { code, headers, body, content_type })
79}
80
81fn parse_range(range: &str) -> Option<(u64, u64)> {
82 let (start, end) = range.strip_prefix("bytes=")?.split_once('-')?;
83 let start = if start.is_empty() { 0 } else { start.parse().ok()? };
84 let end = if end.is_empty() { u64::MAX } else { end.parse().ok()? };
85 Some((start, end))
86}
87
88static CONTENT_TYPES: LazyLock<HashMap<&'static OsStr, &'static str>> = LazyLock::new(|| HashMap::from([
90 (os!("html"), "text/html"),
92 (os!("htm"), "text/html"),
93 (os!("css"), "text/css"),
94 (os!("js"), "application/javascript"),
95 (os!("txt"), "text/plain"),
96 (os!("xml"), "text/xml"),
97 (os!("json"), "application/json"),
98 (os!("wasm"), "application/wasm"),
99 (os!("png"), "image/png"),
101 (os!("jpg"), "image/jpeg"),
102 (os!("jpeg"), "image/jpeg"),
103 (os!("webp"), "image/webp"),
104 (os!("avif"), "image/avif"),
105 (os!("jxl"), "image/jxl"),
106 (os!("gif"), "image/gif"),
107 (os!("svg"), "image/svg+xml"),
108 (os!("svgz"), "image/svg+xml"),
109 (os!("mp4"), "video/mp4"),
111 (os!("mkv"), "video/matroska"),
112 (os!("webm"), "video/webm"),
113 (os!("avi"), "video/x-msvideo"),
114 (os!("m3u8"), "application/vnd.apple.mpegurl"),
115 (os!("mov"), "video/quicktime"),
116 (os!("mp3"), "audio/mpeg"),
118 (os!("ogg"), "audio/ogg"),
119 (os!("m4a"), "audio/mp4"),
120 (os!("woff"), "font/woff"),
122 (os!("woff2"), "font/woff2"),
123 (os!("pdf"), "application/pdf"),
125]));
126
127fn get_content_type(ext: Option<&OsStr>) -> Option<&'static str> {
128 CONTENT_TYPES.get(ext?.to_ascii_lowercase().as_os_str()).copied()
129}
130
131macro_rules! os {
132 ($s:literal) => { OsStr::new($s) }
133}
134use os;