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