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 #[tokio::test]
161 async fn basic() {
162 let svc = ServeFile::new("../README.md");
163
164 let res = svc.oneshot(Request::new(Body::empty())).await.unwrap();
165
166 assert_eq!(res.headers()["content-type"], "text/markdown");
167
168 let body = res.into_body().collect().await.unwrap().to_bytes();
169 let body = String::from_utf8(body.to_vec()).unwrap();
170
171 assert!(body.starts_with("# Tower HTTP"));
172 }
173
174 #[tokio::test]
175 async fn basic_with_mime() {
176 let svc = ServeFile::new_with_mime("../README.md", &Mime::from_str("image/jpg").unwrap());
177
178 let res = svc.oneshot(Request::new(Body::empty())).await.unwrap();
179
180 assert_eq!(res.headers()["content-type"], "image/jpg");
181
182 let body = res.into_body().collect().await.unwrap().to_bytes();
183 let body = String::from_utf8(body.to_vec()).unwrap();
184
185 assert!(body.starts_with("# Tower HTTP"));
186 }
187
188 #[tokio::test]
189 async fn head_request() {
190 let svc = ServeFile::new("../test-files/precompressed.txt");
191
192 let mut request = Request::new(Body::empty());
193 *request.method_mut() = Method::HEAD;
194 let res = svc.oneshot(request).await.unwrap();
195
196 assert_eq!(res.headers()["content-type"], "text/plain");
197 assert_eq!(res.headers()["content-length"], "23");
198
199 assert!(res.into_body().frame().await.is_none());
200 }
201
202 #[tokio::test]
203 async fn precompresed_head_request() {
204 let svc = ServeFile::new("../test-files/precompressed.txt").precompressed_gzip();
205
206 let request = Request::builder()
207 .header("Accept-Encoding", "gzip")
208 .method(Method::HEAD)
209 .body(Body::empty())
210 .unwrap();
211 let res = svc.oneshot(request).await.unwrap();
212
213 assert_eq!(res.headers()["content-type"], "text/plain");
214 assert_eq!(res.headers()["content-encoding"], "gzip");
215 assert_eq!(res.headers()["content-length"], "59");
216
217 assert!(res.into_body().frame().await.is_none());
218 }
219
220 #[tokio::test]
221 async fn precompressed_gzip() {
222 let svc = ServeFile::new("../test-files/precompressed.txt").precompressed_gzip();
223
224 let request = Request::builder()
225 .header("Accept-Encoding", "gzip")
226 .body(Body::empty())
227 .unwrap();
228 let res = svc.oneshot(request).await.unwrap();
229
230 assert_eq!(res.headers()["content-type"], "text/plain");
231 assert_eq!(res.headers()["content-encoding"], "gzip");
232
233 let body = res.into_body().collect().await.unwrap().to_bytes();
234 let mut decoder = GzDecoder::new(&body[..]);
235 let mut decompressed = String::new();
236 decoder.read_to_string(&mut decompressed).unwrap();
237 assert!(decompressed.starts_with("\"This is a test file!\""));
238 }
239
240 #[tokio::test]
241 async fn unsupported_precompression_alogrithm_fallbacks_to_uncompressed() {
242 let svc = ServeFile::new("../test-files/precompressed.txt").precompressed_gzip();
243
244 let request = Request::builder()
245 .header("Accept-Encoding", "br")
246 .body(Body::empty())
247 .unwrap();
248 let res = svc.oneshot(request).await.unwrap();
249
250 assert_eq!(res.headers()["content-type"], "text/plain");
251 assert!(res.headers().get("content-encoding").is_none());
252
253 let body = res.into_body().collect().await.unwrap().to_bytes();
254 let body = String::from_utf8(body.to_vec()).unwrap();
255 assert!(body.starts_with("\"This is a test file!\""));
256 }
257
258 #[tokio::test]
259 async fn missing_precompressed_variant_fallbacks_to_uncompressed() {
260 let svc = ServeFile::new("../test-files/missing_precompressed.txt").precompressed_gzip();
261
262 let request = Request::builder()
263 .header("Accept-Encoding", "gzip")
264 .body(Body::empty())
265 .unwrap();
266 let res = svc.oneshot(request).await.unwrap();
267
268 assert_eq!(res.headers()["content-type"], "text/plain");
269 assert!(res.headers().get("content-encoding").is_none());
271
272 let body = res.into_body().collect().await.unwrap().to_bytes();
273 let body = String::from_utf8(body.to_vec()).unwrap();
274 assert!(body.starts_with("Test file!"));
275 }
276
277 #[tokio::test]
278 async fn missing_precompressed_variant_fallbacks_to_uncompressed_head_request() {
279 let svc = ServeFile::new("../test-files/missing_precompressed.txt").precompressed_gzip();
280
281 let request = Request::builder()
282 .header("Accept-Encoding", "gzip")
283 .method(Method::HEAD)
284 .body(Body::empty())
285 .unwrap();
286 let res = svc.oneshot(request).await.unwrap();
287
288 assert_eq!(res.headers()["content-type"], "text/plain");
289 assert_eq!(res.headers()["content-length"], "11");
290 assert!(res.headers().get("content-encoding").is_none());
292
293 assert!(res.into_body().frame().await.is_none());
294 }
295
296 #[tokio::test]
297 async fn only_precompressed_variant_existing() {
298 let svc = ServeFile::new("../test-files/only_gzipped.txt").precompressed_gzip();
299
300 let request = Request::builder().body(Body::empty()).unwrap();
301 let res = svc.clone().oneshot(request).await.unwrap();
302
303 assert_eq!(res.status(), StatusCode::NOT_FOUND);
304
305 let request = Request::builder()
307 .header("Accept-Encoding", "gzip")
308 .body(Body::empty())
309 .unwrap();
310 let res = svc.oneshot(request).await.unwrap();
311
312 assert_eq!(res.headers()["content-type"], "text/plain");
313 assert_eq!(res.headers()["content-encoding"], "gzip");
314
315 let body = res.into_body().collect().await.unwrap().to_bytes();
316 let mut decoder = GzDecoder::new(&body[..]);
317 let mut decompressed = String::new();
318 decoder.read_to_string(&mut decompressed).unwrap();
319 assert!(decompressed.starts_with("\"This is a test file\""));
320 }
321
322 #[tokio::test]
323 async fn precompressed_br() {
324 let svc = ServeFile::new("../test-files/precompressed.txt").precompressed_br();
325
326 let request = Request::builder()
327 .header("Accept-Encoding", "gzip,br")
328 .body(Body::empty())
329 .unwrap();
330 let res = svc.oneshot(request).await.unwrap();
331
332 assert_eq!(res.headers()["content-type"], "text/plain");
333 assert_eq!(res.headers()["content-encoding"], "br");
334
335 let body = res.into_body().collect().await.unwrap().to_bytes();
336 let mut decompressed = Vec::new();
337 BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
338 let decompressed = String::from_utf8(decompressed.to_vec()).unwrap();
339 assert!(decompressed.starts_with("\"This is a test file!\""));
340 }
341
342 #[tokio::test]
343 async fn precompressed_deflate() {
344 let svc = ServeFile::new("../test-files/precompressed.txt").precompressed_deflate();
345 let request = Request::builder()
346 .header("Accept-Encoding", "deflate,br")
347 .body(Body::empty())
348 .unwrap();
349 let res = svc.oneshot(request).await.unwrap();
350
351 assert_eq!(res.headers()["content-type"], "text/plain");
352 assert_eq!(res.headers()["content-encoding"], "deflate");
353
354 let body = res.into_body().collect().await.unwrap().to_bytes();
355 let mut decoder = DeflateDecoder::new(&body[..]);
356 let mut decompressed = String::new();
357 decoder.read_to_string(&mut decompressed).unwrap();
358 assert!(decompressed.starts_with("\"This is a test file!\""));
359 }
360
361 #[tokio::test]
362 async fn precompressed_zstd() {
363 let svc = ServeFile::new("../test-files/precompressed.txt").precompressed_zstd();
364 let request = Request::builder()
365 .header("Accept-Encoding", "zstd,br")
366 .body(Body::empty())
367 .unwrap();
368 let res = svc.oneshot(request).await.unwrap();
369
370 assert_eq!(res.headers()["content-type"], "text/plain");
371 assert_eq!(res.headers()["content-encoding"], "zstd");
372
373 let body = res.into_body().collect().await.unwrap().to_bytes();
374 let mut decoder = ZstdDecoder::new(&body[..]);
375 let mut decompressed = String::new();
376 decoder.read_to_string(&mut decompressed).await.unwrap();
377 assert!(decompressed.starts_with("\"This is a test file!\""));
378 }
379
380 #[tokio::test]
381 async fn multi_precompressed() {
382 let svc = ServeFile::new("../test-files/precompressed.txt")
383 .precompressed_gzip()
384 .precompressed_br();
385
386 let request = Request::builder()
387 .header("Accept-Encoding", "gzip")
388 .body(Body::empty())
389 .unwrap();
390 let res = svc.clone().oneshot(request).await.unwrap();
391
392 assert_eq!(res.headers()["content-type"], "text/plain");
393 assert_eq!(res.headers()["content-encoding"], "gzip");
394
395 let body = res.into_body().collect().await.unwrap().to_bytes();
396 let mut decoder = GzDecoder::new(&body[..]);
397 let mut decompressed = String::new();
398 decoder.read_to_string(&mut decompressed).unwrap();
399 assert!(decompressed.starts_with("\"This is a test file!\""));
400
401 let request = Request::builder()
402 .header("Accept-Encoding", "br")
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"], "br");
409
410 let body = res.into_body().collect().await.unwrap().to_bytes();
411 let mut decompressed = Vec::new();
412 BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
413 let decompressed = String::from_utf8(decompressed.to_vec()).unwrap();
414 assert!(decompressed.starts_with("\"This is a test file!\""));
415 }
416
417 #[tokio::test]
418 async fn with_custom_chunk_size() {
419 let svc = ServeFile::new("../README.md").with_buf_chunk_size(1024 * 32);
420
421 let res = svc.oneshot(Request::new(Body::empty())).await.unwrap();
422
423 assert_eq!(res.headers()["content-type"], "text/markdown");
424
425 let body = res.into_body().collect().await.unwrap().to_bytes();
426 let body = String::from_utf8(body.to_vec()).unwrap();
427
428 assert!(body.starts_with("# Tower HTTP"));
429 }
430
431 #[tokio::test]
432 async fn fallbacks_to_different_precompressed_variant_if_not_found() {
433 let svc = ServeFile::new("../test-files/precompressed_br.txt")
434 .precompressed_gzip()
435 .precompressed_deflate()
436 .precompressed_br();
437
438 let request = Request::builder()
439 .header("Accept-Encoding", "gzip,deflate,br")
440 .body(Body::empty())
441 .unwrap();
442 let res = svc.oneshot(request).await.unwrap();
443
444 assert_eq!(res.headers()["content-type"], "text/plain");
445 assert_eq!(res.headers()["content-encoding"], "br");
446
447 let body = res.into_body().collect().await.unwrap().to_bytes();
448 let mut decompressed = Vec::new();
449 BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
450 let decompressed = String::from_utf8(decompressed.to_vec()).unwrap();
451 assert!(decompressed.starts_with("Test file"));
452 }
453
454 #[tokio::test]
455 async fn fallbacks_to_different_precompressed_variant_if_not_found_head_request() {
456 let svc = ServeFile::new("../test-files/precompressed_br.txt")
457 .precompressed_gzip()
458 .precompressed_deflate()
459 .precompressed_br();
460
461 let request = Request::builder()
462 .header("Accept-Encoding", "gzip,deflate,br")
463 .method(Method::HEAD)
464 .body(Body::empty())
465 .unwrap();
466 let res = svc.oneshot(request).await.unwrap();
467
468 assert_eq!(res.headers()["content-type"], "text/plain");
469 assert_eq!(res.headers()["content-length"], "15");
470 assert_eq!(res.headers()["content-encoding"], "br");
471
472 assert!(res.into_body().frame().await.is_none());
473 }
474
475 #[tokio::test]
476 async fn returns_404_if_file_doesnt_exist() {
477 let svc = ServeFile::new("../this-doesnt-exist.md");
478
479 let res = svc.oneshot(Request::new(Body::empty())).await.unwrap();
480
481 assert_eq!(res.status(), StatusCode::NOT_FOUND);
482 assert!(res.headers().get(header::CONTENT_TYPE).is_none());
483 }
484
485 #[tokio::test]
486 async fn returns_404_if_file_doesnt_exist_when_precompression_is_used() {
487 let svc = ServeFile::new("../this-doesnt-exist.md").precompressed_deflate();
488
489 let request = Request::builder()
490 .header("Accept-Encoding", "deflate")
491 .body(Body::empty())
492 .unwrap();
493 let res = svc.oneshot(request).await.unwrap();
494
495 assert_eq!(res.status(), StatusCode::NOT_FOUND);
496 assert!(res.headers().get(header::CONTENT_TYPE).is_none());
497 }
498
499 #[tokio::test]
500 async fn last_modified() {
501 let svc = ServeFile::new("../README.md");
502
503 let req = Request::builder().body(Body::empty()).unwrap();
504 let res = svc.oneshot(req).await.unwrap();
505
506 assert_eq!(res.status(), StatusCode::OK);
507
508 let last_modified = res
509 .headers()
510 .get(header::LAST_MODIFIED)
511 .expect("Missing last modified header!");
512
513 let svc = ServeFile::new("../README.md");
516 let req = Request::builder()
517 .header(header::IF_MODIFIED_SINCE, last_modified)
518 .body(Body::empty())
519 .unwrap();
520
521 let res = svc.oneshot(req).await.unwrap();
522 assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
523 assert!(res.into_body().frame().await.is_none());
524
525 let svc = ServeFile::new("../README.md");
526 let req = Request::builder()
527 .header(header::IF_MODIFIED_SINCE, "Fri, 09 Aug 1996 14:21:40 GMT")
528 .body(Body::empty())
529 .unwrap();
530
531 let res = svc.oneshot(req).await.unwrap();
532 assert_eq!(res.status(), StatusCode::OK);
533 let readme_bytes = include_bytes!("../../../../README.md");
534 let body = res.into_body().collect().await.unwrap().to_bytes();
535 assert_eq!(body.as_ref(), readme_bytes);
536
537 let svc = ServeFile::new("../README.md");
540 let req = Request::builder()
541 .header(header::IF_UNMODIFIED_SINCE, last_modified)
542 .body(Body::empty())
543 .unwrap();
544
545 let res = svc.oneshot(req).await.unwrap();
546 assert_eq!(res.status(), StatusCode::OK);
547 let body = res.into_body().collect().await.unwrap().to_bytes();
548 assert_eq!(body.as_ref(), readme_bytes);
549
550 let svc = ServeFile::new("../README.md");
551 let req = Request::builder()
552 .header(header::IF_UNMODIFIED_SINCE, "Fri, 09 Aug 1996 14:21:40 GMT")
553 .body(Body::empty())
554 .unwrap();
555
556 let res = svc.oneshot(req).await.unwrap();
557 assert_eq!(res.status(), StatusCode::PRECONDITION_FAILED);
558 assert!(res.into_body().frame().await.is_none());
559 }
560}