1use super::ServeDir;
4use http::{HeaderValue, Request};
5use mime::Mime;
6use std::{
7 path::Path,
8 task::{Context, Poll},
9};
10use tower_service::Service;
11
12#[derive(Clone, Debug)]
14pub struct ServeFile(ServeDir);
15
16impl ServeFile {
18 pub fn new<P: AsRef<Path>>(path: P) -> Self {
22 let guess = mime_guess::from_path(path.as_ref());
23 let mime = guess
24 .first_raw()
25 .map(HeaderValue::from_static)
26 .unwrap_or_else(|| {
27 HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap()
28 });
29
30 Self(ServeDir::new_single_file(path, mime))
31 }
32
33 pub fn new_with_mime<P: AsRef<Path>>(path: P, mime: &Mime) -> Self {
41 let mime = HeaderValue::from_str(mime.as_ref()).expect("mime isn't a valid header value");
42 Self(ServeDir::new_single_file(path, mime))
43 }
44
45 pub fn precompressed_gzip(self) -> Self {
56 Self(self.0.precompressed_gzip())
57 }
58
59 pub fn precompressed_br(self) -> Self {
70 Self(self.0.precompressed_br())
71 }
72
73 pub fn precompressed_deflate(self) -> Self {
84 Self(self.0.precompressed_deflate())
85 }
86
87 pub fn precompressed_zstd(self) -> Self {
98 Self(self.0.precompressed_zstd())
99 }
100
101 pub fn with_buf_chunk_size(self, chunk_size: usize) -> Self {
105 Self(self.0.with_buf_chunk_size(chunk_size))
106 }
107
108 pub fn ignore_multi_range_requests(self, ignore: bool) -> Self {
112 Self(self.0.ignore_multi_range_requests(ignore))
113 }
114
115 pub fn try_call<ReqBody>(
120 &mut self,
121 req: Request<ReqBody>,
122 ) -> super::serve_dir::future::ResponseFuture<ReqBody>
123 where
124 ReqBody: Send + 'static,
125 {
126 self.0.try_call(req)
127 }
128}
129
130impl<ReqBody> Service<Request<ReqBody>> for ServeFile
131where
132 ReqBody: Send + 'static,
133{
134 type Error = <ServeDir as Service<Request<ReqBody>>>::Error;
135 type Response = <ServeDir as Service<Request<ReqBody>>>::Response;
136 type Future = <ServeDir as Service<Request<ReqBody>>>::Future;
137
138 #[inline]
139 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
140 Poll::Ready(Ok(()))
141 }
142
143 #[inline]
144 fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
145 self.0.call(req)
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use crate::services::ServeFile;
152 use crate::test_helpers::Body;
153 use brotli::BrotliDecompress;
154 use flate2::bufread::DeflateDecoder;
155 use flate2::bufread::GzDecoder;
156 use http::header;
157 use http::Method;
158 use http::{Request, StatusCode};
159 use http_body_util::BodyExt;
160 use mime::Mime;
161 use std::io::Read;
162 use std::str::FromStr;
163 use tower::ServiceExt;
164
165 const EXPECTED_CONTENT_PREFIX: &str = "Test file";
167
168 const TEST_FILES_DIR: &str = "../test-files";
170 const README_PATH: &str = "../README.md";
172
173 #[tokio::test]
174 async fn basic() {
175 let svc = ServeFile::new(README_PATH);
176
177 let res = svc.oneshot(Request::new(Body::empty())).await.unwrap();
178
179 assert_eq!(res.headers()["content-type"], "text/markdown");
180
181 let body = res.into_body().collect().await.unwrap().to_bytes();
182 let body = String::from_utf8(body.to_vec()).unwrap();
183
184 assert!(body.starts_with("# Tower HTTP"));
185 }
186
187 #[tokio::test]
188 async fn basic_with_mime() {
189 let svc = ServeFile::new_with_mime(README_PATH, &Mime::from_str("image/jpg").unwrap());
190
191 let res = svc.oneshot(Request::new(Body::empty())).await.unwrap();
192
193 assert_eq!(res.headers()["content-type"], "image/jpg");
194
195 let body = res.into_body().collect().await.unwrap().to_bytes();
196 let body = String::from_utf8(body.to_vec()).unwrap();
197
198 assert!(body.starts_with("# Tower HTTP"));
199 }
200
201 #[tokio::test]
202 async fn multipart_range_can_be_ignored() {
203 let svc = ServeFile::new(README_PATH).ignore_multi_range_requests(true);
204 let request = Request::builder()
205 .header(header::RANGE, "bytes=0-0,2-2")
206 .body(Body::empty())
207 .unwrap();
208 let res = svc.oneshot(request).await.unwrap();
209
210 assert_eq!(res.status(), StatusCode::OK);
211 assert!(res.headers().get(header::CONTENT_RANGE).is_none());
212
213 let body = res.into_body().collect().await.unwrap().to_bytes();
214 assert_eq!(body.as_ref(), std::fs::read(README_PATH).unwrap());
215 }
216
217 #[tokio::test]
218 async fn head_request() {
219 let svc = ServeFile::new(format!("{TEST_FILES_DIR}/precompressed.txt"));
220
221 let mut request = Request::new(Body::empty());
222 *request.method_mut() = Method::HEAD;
223 let res = svc.oneshot(request).await.unwrap();
224
225 assert_eq!(res.headers()["content-type"], "text/plain");
226 assert_eq!(res.headers()["content-length"], "10");
227
228 assert!(res.into_body().frame().await.is_none());
229 }
230
231 #[tokio::test]
232 async fn precompressed_head_request() {
233 let svc =
234 ServeFile::new(format!("{TEST_FILES_DIR}/precompressed.txt")).precompressed_gzip();
235
236 let request = Request::builder()
237 .header("Accept-Encoding", "gzip")
238 .method(Method::HEAD)
239 .body(Body::empty())
240 .unwrap();
241 let res = svc.oneshot(request).await.unwrap();
242
243 assert_eq!(res.headers()["content-type"], "text/plain");
244 assert_eq!(res.headers()["content-encoding"], "gzip");
245 assert_eq!(res.headers()["content-length"], "30");
246
247 assert!(res.into_body().frame().await.is_none());
248 }
249
250 #[tokio::test]
251 async fn precompressed_gzip() {
252 let svc =
253 ServeFile::new(format!("{TEST_FILES_DIR}/precompressed.txt")).precompressed_gzip();
254
255 let request = Request::builder()
256 .header("Accept-Encoding", "gzip")
257 .body(Body::empty())
258 .unwrap();
259 let res = svc.oneshot(request).await.unwrap();
260
261 assert_eq!(res.headers()["content-type"], "text/plain");
262 assert_eq!(res.headers()["content-encoding"], "gzip");
263
264 let body = res.into_body().collect().await.unwrap().to_bytes();
265 let mut decoder = GzDecoder::new(&body[..]);
266 let mut decompressed = String::new();
267 decoder.read_to_string(&mut decompressed).unwrap();
268 assert!(decompressed.starts_with(EXPECTED_CONTENT_PREFIX));
269 }
270
271 #[tokio::test]
272 async fn unsupported_precompression_alogrithm_fallbacks_to_uncompressed() {
273 let svc =
274 ServeFile::new(format!("{TEST_FILES_DIR}/precompressed.txt")).precompressed_gzip();
275
276 let request = Request::builder()
277 .header("Accept-Encoding", "br")
278 .body(Body::empty())
279 .unwrap();
280 let res = svc.oneshot(request).await.unwrap();
281
282 assert_eq!(res.headers()["content-type"], "text/plain");
283 assert!(res.headers().get("content-encoding").is_none());
284
285 let body = res.into_body().collect().await.unwrap().to_bytes();
286 let body = String::from_utf8(body.to_vec()).unwrap();
287 assert!(body.starts_with(EXPECTED_CONTENT_PREFIX));
288 }
289
290 #[tokio::test]
291 async fn missing_precompressed_variant_fallbacks_to_uncompressed() {
292 let svc = ServeFile::new(format!("{TEST_FILES_DIR}/missing_precompressed.txt"))
293 .precompressed_gzip();
294
295 let request = Request::builder()
296 .header("Accept-Encoding", "gzip")
297 .body(Body::empty())
298 .unwrap();
299 let res = svc.oneshot(request).await.unwrap();
300
301 assert_eq!(res.headers()["content-type"], "text/plain");
302 assert!(res.headers().get("content-encoding").is_none());
304
305 let body = res.into_body().collect().await.unwrap().to_bytes();
306 let body = String::from_utf8(body.to_vec()).unwrap();
307 assert!(body.starts_with(EXPECTED_CONTENT_PREFIX));
308 }
309
310 #[tokio::test]
311 async fn missing_precompressed_variant_fallbacks_to_uncompressed_head_request() {
312 let svc = ServeFile::new(format!("{TEST_FILES_DIR}/missing_precompressed.txt"))
313 .precompressed_gzip();
314
315 let request = Request::builder()
316 .header("Accept-Encoding", "gzip")
317 .method(Method::HEAD)
318 .body(Body::empty())
319 .unwrap();
320 let res = svc.oneshot(request).await.unwrap();
321
322 assert_eq!(res.headers()["content-type"], "text/plain");
323 assert_eq!(res.headers()["content-length"], "10");
324 assert!(res.headers().get("content-encoding").is_none());
326
327 assert!(res.into_body().frame().await.is_none());
328 }
329
330 #[tokio::test]
331 async fn only_precompressed_variant_existing() {
332 let svc = ServeFile::new(format!("{TEST_FILES_DIR}/only_gzipped.txt")).precompressed_gzip();
333
334 let request = Request::builder().body(Body::empty()).unwrap();
335 let res = svc.clone().oneshot(request).await.unwrap();
336
337 assert_eq!(res.status(), StatusCode::NOT_FOUND);
338
339 let request = Request::builder()
341 .header("Accept-Encoding", "gzip")
342 .body(Body::empty())
343 .unwrap();
344 let res = svc.oneshot(request).await.unwrap();
345
346 assert_eq!(res.headers()["content-type"], "text/plain");
347 assert_eq!(res.headers()["content-encoding"], "gzip");
348
349 let body = res.into_body().collect().await.unwrap().to_bytes();
350 let mut decoder = GzDecoder::new(&body[..]);
351 let mut decompressed = String::new();
352 decoder.read_to_string(&mut decompressed).unwrap();
353 assert!(decompressed.starts_with(EXPECTED_CONTENT_PREFIX));
354 }
355
356 #[tokio::test]
357 async fn precompressed_br() {
358 let svc = ServeFile::new(format!("{TEST_FILES_DIR}/precompressed.txt")).precompressed_br();
359
360 let request = Request::builder()
361 .header("Accept-Encoding", "gzip,br")
362 .body(Body::empty())
363 .unwrap();
364 let res = svc.oneshot(request).await.unwrap();
365
366 assert_eq!(res.headers()["content-type"], "text/plain");
367 assert_eq!(res.headers()["content-encoding"], "br");
368
369 let body = res.into_body().collect().await.unwrap().to_bytes();
370 let mut decompressed = Vec::new();
371 BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
372 let decompressed = String::from_utf8(decompressed.to_vec()).unwrap();
373 assert!(decompressed.starts_with(EXPECTED_CONTENT_PREFIX));
374 }
375
376 #[tokio::test]
377 async fn precompressed_deflate() {
378 let svc =
379 ServeFile::new(format!("{TEST_FILES_DIR}/precompressed.txt")).precompressed_deflate();
380 let request = Request::builder()
381 .header("Accept-Encoding", "deflate,br")
382 .body(Body::empty())
383 .unwrap();
384 let res = svc.oneshot(request).await.unwrap();
385
386 assert_eq!(res.headers()["content-type"], "text/plain");
387 assert_eq!(res.headers()["content-encoding"], "deflate");
388
389 let body = res.into_body().collect().await.unwrap().to_bytes();
390 let mut decoder = DeflateDecoder::new(&body[..]);
391 let mut decompressed = String::new();
392 decoder.read_to_string(&mut decompressed).unwrap();
393 assert!(decompressed.starts_with(EXPECTED_CONTENT_PREFIX));
394 }
395
396 #[tokio::test]
397 async fn precompressed_zstd() {
398 let svc =
399 ServeFile::new(format!("{TEST_FILES_DIR}/precompressed.txt")).precompressed_zstd();
400 let request = Request::builder()
401 .header("Accept-Encoding", "zstd,br")
402 .body(Body::empty())
403 .unwrap();
404 let res = svc.oneshot(request).await.unwrap();
405
406 assert_eq!(res.headers()["content-type"], "text/plain");
407 assert_eq!(res.headers()["content-encoding"], "zstd");
408
409 let body = res.into_body().collect().await.unwrap().to_bytes();
410 let decompressed = zstd::stream::decode_all(&body[..]).unwrap();
411 let decompressed = String::from_utf8(decompressed).unwrap();
412 assert!(decompressed.starts_with(EXPECTED_CONTENT_PREFIX));
413 }
414
415 #[tokio::test]
416 async fn multi_precompressed() {
417 let svc = ServeFile::new(format!("{TEST_FILES_DIR}/precompressed.txt"))
418 .precompressed_gzip()
419 .precompressed_br();
420
421 let request = Request::builder()
422 .header("Accept-Encoding", "gzip")
423 .body(Body::empty())
424 .unwrap();
425 let res = svc.clone().oneshot(request).await.unwrap();
426
427 assert_eq!(res.headers()["content-type"], "text/plain");
428 assert_eq!(res.headers()["content-encoding"], "gzip");
429
430 let body = res.into_body().collect().await.unwrap().to_bytes();
431 let mut decoder = GzDecoder::new(&body[..]);
432 let mut decompressed = String::new();
433 decoder.read_to_string(&mut decompressed).unwrap();
434 assert!(decompressed.starts_with(EXPECTED_CONTENT_PREFIX));
435
436 let request = Request::builder()
437 .header("Accept-Encoding", "br")
438 .body(Body::empty())
439 .unwrap();
440 let res = svc.clone().oneshot(request).await.unwrap();
441
442 assert_eq!(res.headers()["content-type"], "text/plain");
443 assert_eq!(res.headers()["content-encoding"], "br");
444
445 let body = res.into_body().collect().await.unwrap().to_bytes();
446 let mut decompressed = Vec::new();
447 BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
448 let decompressed = String::from_utf8(decompressed.to_vec()).unwrap();
449 assert!(decompressed.starts_with(EXPECTED_CONTENT_PREFIX));
450 }
451
452 #[tokio::test]
453 async fn with_custom_chunk_size() {
454 let svc = ServeFile::new(README_PATH).with_buf_chunk_size(1024 * 32);
455
456 let res = svc.oneshot(Request::new(Body::empty())).await.unwrap();
457
458 assert_eq!(res.headers()["content-type"], "text/markdown");
459
460 let body = res.into_body().collect().await.unwrap().to_bytes();
461 let body = String::from_utf8(body.to_vec()).unwrap();
462
463 assert!(body.starts_with("# Tower HTTP"));
464 }
465
466 #[tokio::test]
467 async fn fallbacks_to_different_precompressed_variant_if_not_found() {
468 let svc = ServeFile::new(format!("{TEST_FILES_DIR}/precompressed_br.txt"))
469 .precompressed_gzip()
470 .precompressed_deflate()
471 .precompressed_br();
472
473 let request = Request::builder()
474 .header("Accept-Encoding", "gzip,deflate,br")
475 .body(Body::empty())
476 .unwrap();
477 let res = svc.oneshot(request).await.unwrap();
478
479 assert_eq!(res.headers()["content-type"], "text/plain");
480 assert_eq!(res.headers()["content-encoding"], "br");
481
482 let body = res.into_body().collect().await.unwrap().to_bytes();
483 let mut decompressed = Vec::new();
484 BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
485 let decompressed = String::from_utf8(decompressed.to_vec()).unwrap();
486 assert!(decompressed.starts_with(EXPECTED_CONTENT_PREFIX));
487 }
488
489 #[tokio::test]
490 async fn fallbacks_to_different_precompressed_variant_if_not_found_head_request() {
491 let svc = ServeFile::new(format!("{TEST_FILES_DIR}/precompressed_br.txt"))
492 .precompressed_gzip()
493 .precompressed_deflate()
494 .precompressed_br();
495
496 let request = Request::builder()
497 .header("Accept-Encoding", "gzip,deflate,br")
498 .method(Method::HEAD)
499 .body(Body::empty())
500 .unwrap();
501 let res = svc.oneshot(request).await.unwrap();
502
503 assert_eq!(res.headers()["content-type"], "text/plain");
504 assert_eq!(res.headers()["content-length"], "15");
505 assert_eq!(res.headers()["content-encoding"], "br");
506
507 assert!(res.into_body().frame().await.is_none());
508 }
509
510 #[tokio::test]
511 async fn returns_404_if_file_doesnt_exist() {
512 let svc = ServeFile::new("../this-doesnt-exist.md");
513
514 let res = svc.oneshot(Request::new(Body::empty())).await.unwrap();
515
516 assert_eq!(res.status(), StatusCode::NOT_FOUND);
517 assert!(res.headers().get(header::CONTENT_TYPE).is_none());
518 }
519
520 #[tokio::test]
521 async fn returns_404_if_file_doesnt_exist_when_precompression_is_used() {
522 let svc = ServeFile::new("../this-doesnt-exist.md").precompressed_deflate();
523
524 let request = Request::builder()
525 .header("Accept-Encoding", "deflate")
526 .body(Body::empty())
527 .unwrap();
528 let res = svc.oneshot(request).await.unwrap();
529
530 assert_eq!(res.status(), StatusCode::NOT_FOUND);
531 assert!(res.headers().get(header::CONTENT_TYPE).is_none());
532 }
533
534 #[tokio::test]
535 async fn last_modified() {
536 let svc = ServeFile::new(README_PATH);
537
538 let req = Request::builder().body(Body::empty()).unwrap();
539 let res = svc.oneshot(req).await.unwrap();
540
541 assert_eq!(res.status(), StatusCode::OK);
542
543 let last_modified = res
544 .headers()
545 .get(header::LAST_MODIFIED)
546 .expect("Missing last modified header!");
547
548 let svc = ServeFile::new(README_PATH);
551 let req = Request::builder()
552 .header(header::IF_MODIFIED_SINCE, last_modified)
553 .body(Body::empty())
554 .unwrap();
555
556 let res = svc.oneshot(req).await.unwrap();
557 assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
558 assert!(res.into_body().frame().await.is_none());
559
560 let svc = ServeFile::new(README_PATH);
561 let req = Request::builder()
562 .header(header::IF_MODIFIED_SINCE, "Fri, 09 Aug 1996 14:21:40 GMT")
563 .body(Body::empty())
564 .unwrap();
565
566 let res = svc.oneshot(req).await.unwrap();
567 assert_eq!(res.status(), StatusCode::OK);
568 let readme_bytes = include_bytes!("../../../../README.md");
569 let body = res.into_body().collect().await.unwrap().to_bytes();
570 assert_eq!(body.as_ref(), readme_bytes);
571
572 let svc = ServeFile::new(README_PATH);
575 let req = Request::builder()
576 .header(header::IF_UNMODIFIED_SINCE, last_modified)
577 .body(Body::empty())
578 .unwrap();
579
580 let res = svc.oneshot(req).await.unwrap();
581 assert_eq!(res.status(), StatusCode::OK);
582 let body = res.into_body().collect().await.unwrap().to_bytes();
583 assert_eq!(body.as_ref(), readme_bytes);
584
585 let svc = ServeFile::new(README_PATH);
586 let req = Request::builder()
587 .header(header::IF_UNMODIFIED_SINCE, "Fri, 09 Aug 1996 14:21:40 GMT")
588 .body(Body::empty())
589 .unwrap();
590
591 let res = svc.oneshot(req).await.unwrap();
592 assert_eq!(res.status(), StatusCode::PRECONDITION_FAILED);
593 assert!(res.into_body().frame().await.is_none());
594 }
595}