hyper_staticfile/util/
file_response_builder.rs1use std::time::{Duration, SystemTime, UNIX_EPOCH};
2
3use http::{
4 header, response::Builder as ResponseBuilder, HeaderMap, Method, Request, Response, Result,
5 StatusCode,
6};
7use http_range::{HttpRange, HttpRangeParseError};
8use rand::prelude::{thread_rng, SliceRandom};
9
10use crate::{
11 util::{FileBytesStream, FileBytesStreamMultiRange, FileBytesStreamRange},
12 vfs::IntoFileAccess,
13 Body, ResolvedFile,
14};
15
16const MIN_VALID_MTIME: Duration = Duration::from_secs(2);
22
23const BOUNDARY_LENGTH: usize = 60;
24const BOUNDARY_CHARS: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
25
26#[derive(Clone, Debug, Default)]
32pub struct FileResponseBuilder {
33 pub cache_headers: Option<u32>,
35 pub is_head: bool,
37 pub if_modified_since: Option<SystemTime>,
39 pub range: Option<String>,
41 pub if_range: Option<String>,
43}
44
45impl FileResponseBuilder {
46 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn request<B>(&mut self, req: &Request<B>) -> &mut Self {
53 self.request_parts(req.method(), req.headers())
54 }
55
56 pub fn request_parts(&mut self, method: &Method, headers: &HeaderMap) -> &mut Self {
58 self.request_method(method);
59 self.request_headers(headers);
60 self
61 }
62
63 pub fn request_method(&mut self, method: &Method) -> &mut Self {
65 self.is_head = *method == Method::HEAD;
66 self
67 }
68
69 pub fn request_headers(&mut self, headers: &HeaderMap) -> &mut Self {
71 self.if_modified_since_header(headers.get(header::IF_MODIFIED_SINCE));
72 self.range_header(headers.get(header::RANGE));
73 self.if_range(headers.get(header::IF_RANGE));
74 self
75 }
76
77 pub fn cache_headers(&mut self, value: Option<u32>) -> &mut Self {
79 self.cache_headers = value;
80 self
81 }
82
83 pub fn is_head(&mut self, value: bool) -> &mut Self {
85 self.is_head = value;
86 self
87 }
88
89 pub fn if_modified_since(&mut self, value: Option<SystemTime>) -> &mut Self {
91 self.if_modified_since = value;
92 self
93 }
94
95 pub fn if_modified_since_header(&mut self, value: Option<&header::HeaderValue>) -> &mut Self {
97 self.if_modified_since = value
98 .and_then(|v| v.to_str().ok())
99 .and_then(|v| httpdate::parse_http_date(v).ok());
100 self
101 }
102
103 pub fn if_range(&mut self, value: Option<&header::HeaderValue>) -> &mut Self {
105 if let Some(s) = value.and_then(|s| s.to_str().ok()) {
106 self.if_range = Some(s.to_string());
107 }
108 self
109 }
110
111 pub fn range_header(&mut self, value: Option<&header::HeaderValue>) -> &mut Self {
113 self.range = value.and_then(|v| v.to_str().ok()).map(|v| v.to_string());
114 self
115 }
116
117 pub fn build<F: IntoFileAccess>(
119 &self,
120 file: ResolvedFile<F>,
121 ) -> Result<Response<Body<F::Output>>> {
122 let mut res = ResponseBuilder::new();
123
124 let modified = file.modified.filter(|v| {
126 v.duration_since(UNIX_EPOCH)
127 .ok()
128 .filter(|v| v >= &MIN_VALID_MTIME)
129 .is_some()
130 });
131
132 let mut range_cond_ok = self.if_range.is_none();
135 if let Some(modified) = modified {
136 if let Ok(modified_unix) = modified.duration_since(UNIX_EPOCH) {
137 if let Some(Ok(ims_unix)) =
140 self.if_modified_since.map(|v| v.duration_since(UNIX_EPOCH))
141 {
142 if modified_unix.as_secs() <= ims_unix.as_secs() {
143 return ResponseBuilder::new()
144 .status(StatusCode::NOT_MODIFIED)
145 .body(Body::Empty);
146 }
147 }
148
149 let etag = format!(
150 "W/\"{0:x}-{1:x}.{2:x}\"",
151 file.size,
152 modified_unix.as_secs(),
153 modified_unix.subsec_nanos()
154 );
155 if let Some(ref v) = self.if_range {
156 if *v == etag {
157 range_cond_ok = true;
158 }
159 }
160
161 res = res.header(header::ETAG, etag);
162 }
163
164 let last_modified_formatted = httpdate::fmt_http_date(modified);
165 if let Some(ref v) = self.if_range {
166 if *v == last_modified_formatted {
167 range_cond_ok = true;
168 }
169 }
170
171 res = res
172 .header(header::LAST_MODIFIED, last_modified_formatted)
173 .header(header::ACCEPT_RANGES, "bytes");
174 }
175
176 if let Some(seconds) = self.cache_headers {
178 res = res.header(
179 header::CACHE_CONTROL,
180 format!("public, max-age={}", seconds),
181 );
182 }
183
184 if self.is_head {
185 res = res.header(header::CONTENT_LENGTH, format!("{}", file.size));
186 return res.status(StatusCode::OK).body(Body::Empty);
187 }
188
189 let ranges = self.range.as_ref().filter(|_| range_cond_ok).and_then(|r| {
190 match HttpRange::parse(r, file.size) {
191 Ok(r) => Some(Ok(r)),
192 Err(HttpRangeParseError::NoOverlap) => Some(Err(())),
193 Err(HttpRangeParseError::InvalidRange) => None,
194 }
195 });
196
197 if let Some(ranges) = ranges {
198 let ranges = match ranges {
199 Ok(r) => r,
200 Err(()) => {
201 return res
202 .status(StatusCode::RANGE_NOT_SATISFIABLE)
203 .body(Body::Empty);
204 }
205 };
206
207 if ranges.len() == 1 {
208 let single_span = ranges[0];
209 res = res
210 .header(
211 header::CONTENT_RANGE,
212 content_range_header(&single_span, file.size),
213 )
214 .header(header::CONTENT_LENGTH, format!("{}", single_span.length));
215
216 let body_stream =
217 FileBytesStreamRange::new(file.handle.into_file_access(), single_span);
218 return res
219 .status(StatusCode::PARTIAL_CONTENT)
220 .body(Body::Range(body_stream));
221 } else if ranges.len() > 1 {
222 let mut boundary_tmp = [0u8; BOUNDARY_LENGTH];
223
224 let mut rng = thread_rng();
225 for v in boundary_tmp.iter_mut() {
226 *v = *BOUNDARY_CHARS.choose(&mut rng).unwrap();
228 }
229
230 let boundary = std::str::from_utf8(&boundary_tmp[..]).unwrap().to_string();
232
233 res = res.header(
234 hyper::header::CONTENT_TYPE,
235 format!("multipart/byteranges; boundary={}", boundary),
236 );
237
238 let mut body_stream = FileBytesStreamMultiRange::new(
239 file.handle.into_file_access(),
240 ranges,
241 boundary,
242 file.size,
243 );
244 if let Some(content_type) = file.content_type.as_ref() {
245 body_stream.set_content_type(content_type);
246 }
247
248 res = res.header(
249 hyper::header::CONTENT_LENGTH,
250 format!("{}", body_stream.compute_length()),
251 );
252
253 return res
254 .status(StatusCode::PARTIAL_CONTENT)
255 .body(Body::MultiRange(body_stream));
256 }
257 }
258
259 res = res.header(header::CONTENT_LENGTH, format!("{}", file.size));
260 if let Some(content_type) = file.content_type {
261 res = res.header(header::CONTENT_TYPE, content_type);
262 }
263 if let Some(encoding) = file.encoding {
264 res = res.header(header::CONTENT_ENCODING, encoding.to_header_value());
265 }
266
267 res.status(StatusCode::OK)
269 .body(Body::Full(FileBytesStream::new_with_limit(
270 file.handle.into_file_access(),
271 file.size,
272 )))
273 }
274}
275
276fn content_range_header(r: &HttpRange, total_length: u64) -> String {
277 format!(
278 "bytes {}-{}/{}",
279 r.start,
280 r.start + r.length - 1,
281 total_length
282 )
283}