tower_async_http/services/fs/
serve_file.rs1use super::ServeDir;
4use http::{HeaderValue, Request, Response};
5use mime::Mime;
6use std::path::Path;
7use tower_async_service::Service;
8
9#[derive(Clone, Debug)]
11pub struct ServeFile(ServeDir);
12
13impl ServeFile {
15 pub fn new<P: AsRef<Path>>(path: P) -> Self {
19 let guess = mime_guess::from_path(path.as_ref());
20 let mime = guess
21 .first_raw()
22 .map(HeaderValue::from_static)
23 .unwrap_or_else(|| {
24 HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap()
25 });
26
27 ServeFile(ServeDir::new_single_file(path, mime))
28 }
29
30 pub fn new_with_mime<P: AsRef<Path>>(path: P, mime: &Mime) -> Self {
38 let mime = HeaderValue::from_str(mime.as_ref()).expect("mime isn't a valid header value");
39 ServeFile(ServeDir::new_single_file(path, mime))
40 }
41
42 pub fn precompressed_gzip(self) -> Self {
53 Self(self.0.precompressed_gzip())
54 }
55
56 pub fn precompressed_br(self) -> Self {
67 Self(self.0.precompressed_br())
68 }
69
70 pub fn precompressed_deflate(self) -> Self {
81 Self(self.0.precompressed_deflate())
82 }
83
84 pub fn with_buf_chunk_size(self, chunk_size: usize) -> Self {
88 Self(self.0.with_buf_chunk_size(chunk_size))
89 }
90
91 #[inline]
96 pub async fn try_call<ReqBody>(
97 &self,
98 req: Request<ReqBody>,
99 ) -> Result<Response<super::serve_dir::ResponseBody>, std::io::Error>
100 where
101 ReqBody: Send + 'static,
102 {
103 self.0.try_call(req).await
104 }
105}
106
107impl<ReqBody> Service<Request<ReqBody>> for ServeFile
108where
109 ReqBody: Send + 'static,
110{
111 type Error = <ServeDir as Service<Request<ReqBody>>>::Error;
112 type Response = <ServeDir as Service<Request<ReqBody>>>::Response;
113
114 #[inline]
115 async fn call(&self, req: Request<ReqBody>) -> Result<Self::Response, Self::Error> {
116 self.0.call(req).await
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use std::io::Read;
123 use std::str::FromStr;
124
125 use crate::services::ServeFile;
126 use crate::test_helpers::Body;
127
128 use brotli::BrotliDecompress;
129 use flate2::bufread::DeflateDecoder;
130 use flate2::bufread::GzDecoder;
131 use http::header;
132 use http::Method;
133 use http::{Request, StatusCode};
134 use http_body_util::BodyExt;
135 use mime::Mime;
136 use tower_async::ServiceExt;
137
138 #[tokio::test]
139 async fn basic() {
140 let svc = ServeFile::new("./README.md");
141
142 let res = svc.oneshot(Request::new(Body::empty())).await.unwrap();
143
144 assert_eq!(res.headers()["content-type"], "text/markdown");
145
146 let body = res.into_body().collect().await.unwrap().to_bytes();
147 let body = String::from_utf8(body.to_vec()).unwrap();
148
149 assert!(body.starts_with("# Tower Async HTTP"));
150 }
151
152 #[tokio::test]
153 async fn basic_with_mime() {
154 let svc = ServeFile::new_with_mime("./README.md", &Mime::from_str("image/jpg").unwrap());
155
156 let res = svc.oneshot(Request::new(Body::empty())).await.unwrap();
157
158 assert_eq!(res.headers()["content-type"], "image/jpg");
159
160 let body = res.into_body().collect().await.unwrap().to_bytes();
161 let body = String::from_utf8(body.to_vec()).unwrap();
162
163 assert!(body.starts_with("# Tower Async HTTP"));
164 }
165
166 #[tokio::test]
167 async fn head_request() {
168 let svc = ServeFile::new("./test-files/precompressed.txt");
169
170 let mut request = Request::new(Body::empty());
171 *request.method_mut() = Method::HEAD;
172 let res = svc.oneshot(request).await.unwrap();
173
174 assert_eq!(res.headers()["content-type"], "text/plain");
175 assert_eq!(res.headers()["content-length"], "23");
176
177 assert!(res.into_body().frame().await.is_none());
178 }
179
180 #[tokio::test]
181 async fn precompresed_head_request() {
182 let svc = ServeFile::new("./test-files/precompressed.txt").precompressed_gzip();
183
184 let request = Request::builder()
185 .header("Accept-Encoding", "gzip")
186 .method(Method::HEAD)
187 .body(Body::empty())
188 .unwrap();
189 let res = svc.oneshot(request).await.unwrap();
190
191 assert_eq!(res.headers()["content-type"], "text/plain");
192 assert_eq!(res.headers()["content-encoding"], "gzip");
193 assert_eq!(res.headers()["content-length"], "59");
194
195 assert!(res.into_body().frame().await.is_none());
196 }
197
198 #[tokio::test]
199 async fn precompressed_gzip() {
200 let svc = ServeFile::new("./test-files/precompressed.txt").precompressed_gzip();
201
202 let request = Request::builder()
203 .header("Accept-Encoding", "gzip")
204 .body(Body::empty())
205 .unwrap();
206 let res = svc.oneshot(request).await.unwrap();
207
208 assert_eq!(res.headers()["content-type"], "text/plain");
209 assert_eq!(res.headers()["content-encoding"], "gzip");
210
211 let body = res.into_body().collect().await.unwrap().to_bytes();
212 let mut decoder = GzDecoder::new(&body[..]);
213 let mut decompressed = String::new();
214 decoder.read_to_string(&mut decompressed).unwrap();
215 assert!(decompressed.starts_with("\"This is a test file!\""));
216 }
217
218 #[tokio::test]
219 async fn unsupported_precompression_algorithm_fallbacks_to_uncompressed() {
220 let svc = ServeFile::new("./test-files/precompressed.txt").precompressed_gzip();
221
222 let request = Request::builder()
223 .header("Accept-Encoding", "br")
224 .body(Body::empty())
225 .unwrap();
226 let res = svc.oneshot(request).await.unwrap();
227
228 assert_eq!(res.headers()["content-type"], "text/plain");
229 assert!(res.headers().get("content-encoding").is_none());
230
231 let body = res.into_body().collect().await.unwrap().to_bytes();
232 let body = String::from_utf8(body.to_vec()).unwrap();
233 assert!(body.starts_with("\"This is a test file!\""));
234 }
235
236 #[tokio::test]
237 async fn missing_precompressed_variant_fallbacks_to_uncompressed() {
238 let svc = ServeFile::new("./test-files/missing_precompressed.txt").precompressed_gzip();
239
240 let request = Request::builder()
241 .header("Accept-Encoding", "gzip")
242 .body(Body::empty())
243 .unwrap();
244 let res = svc.oneshot(request).await.unwrap();
245
246 assert_eq!(res.headers()["content-type"], "text/plain");
247 assert!(res.headers().get("content-encoding").is_none());
249
250 let body = res.into_body().collect().await.unwrap().to_bytes();
251 let body = String::from_utf8(body.to_vec()).unwrap();
252 assert!(body.starts_with("Test file!"));
253 }
254
255 #[tokio::test]
256 async fn missing_precompressed_variant_fallbacks_to_uncompressed_head_request() {
257 let svc = ServeFile::new("./test-files/missing_precompressed.txt").precompressed_gzip();
258
259 let request = Request::builder()
260 .header("Accept-Encoding", "gzip")
261 .method(Method::HEAD)
262 .body(Body::empty())
263 .unwrap();
264 let res = svc.oneshot(request).await.unwrap();
265
266 assert_eq!(res.headers()["content-type"], "text/plain");
267 assert_eq!(res.headers()["content-length"], "11");
268 assert!(res.headers().get("content-encoding").is_none());
270
271 assert!(res.into_body().frame().await.is_none());
272 }
273
274 #[tokio::test]
275 async fn only_precompressed_variant_existing() {
276 let svc = ServeFile::new("./test-files/only_gzipped.txt").precompressed_gzip();
277
278 let request = Request::builder().body(Body::empty()).unwrap();
279 let res = svc.clone().oneshot(request).await.unwrap();
280
281 assert_eq!(res.status(), StatusCode::NOT_FOUND);
282
283 let request = Request::builder()
285 .header("Accept-Encoding", "gzip")
286 .body(Body::empty())
287 .unwrap();
288 let res = svc.oneshot(request).await.unwrap();
289
290 assert_eq!(res.headers()["content-type"], "text/plain");
291 assert_eq!(res.headers()["content-encoding"], "gzip");
292
293 let body = res.into_body().collect().await.unwrap().to_bytes();
294 let mut decoder = GzDecoder::new(&body[..]);
295 let mut decompressed = String::new();
296 decoder.read_to_string(&mut decompressed).unwrap();
297 assert!(decompressed.starts_with("\"This is a test file\""));
298 }
299
300 #[tokio::test]
301 async fn precompressed_br() {
302 let svc = ServeFile::new("./test-files/precompressed.txt").precompressed_br();
303
304 let request = Request::builder()
305 .header("Accept-Encoding", "gzip,br")
306 .body(Body::empty())
307 .unwrap();
308 let res = svc.oneshot(request).await.unwrap();
309
310 assert_eq!(res.headers()["content-type"], "text/plain");
311 assert_eq!(res.headers()["content-encoding"], "br");
312
313 let body = res.into_body().collect().await.unwrap().to_bytes();
314 let mut decompressed = Vec::new();
315 BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
316 let decompressed = String::from_utf8(decompressed.to_vec()).unwrap();
317 assert!(decompressed.starts_with("\"This is a test file!\""));
318 }
319
320 #[tokio::test]
321 async fn precompressed_deflate() {
322 let svc = ServeFile::new("./test-files/precompressed.txt").precompressed_deflate();
323 let request = Request::builder()
324 .header("Accept-Encoding", "deflate,br")
325 .body(Body::empty())
326 .unwrap();
327 let res = svc.oneshot(request).await.unwrap();
328
329 assert_eq!(res.headers()["content-type"], "text/plain");
330 assert_eq!(res.headers()["content-encoding"], "deflate");
331
332 let body = res.into_body().collect().await.unwrap().to_bytes();
333 let mut decoder = DeflateDecoder::new(&body[..]);
334 let mut decompressed = String::new();
335 decoder.read_to_string(&mut decompressed).unwrap();
336 assert!(decompressed.starts_with("\"This is a test file!\""));
337 }
338
339 #[tokio::test]
340 async fn multi_precompressed() {
341 let svc = ServeFile::new("./test-files/precompressed.txt")
342 .precompressed_gzip()
343 .precompressed_br();
344
345 let request = Request::builder()
346 .header("Accept-Encoding", "gzip")
347 .body(Body::empty())
348 .unwrap();
349 let res = svc.clone().oneshot(request).await.unwrap();
350
351 assert_eq!(res.headers()["content-type"], "text/plain");
352 assert_eq!(res.headers()["content-encoding"], "gzip");
353
354 let body = res.into_body().collect().await.unwrap().to_bytes();
355 let mut decoder = GzDecoder::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 let request = Request::builder()
361 .header("Accept-Encoding", "br")
362 .body(Body::empty())
363 .unwrap();
364 let res = svc.clone().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("\"This is a test file!\""));
374 }
375
376 #[tokio::test]
377 async fn with_custom_chunk_size() {
378 let svc = ServeFile::new("./README.md").with_buf_chunk_size(1024 * 32);
379
380 let res = svc.oneshot(Request::new(Body::empty())).await.unwrap();
381
382 assert_eq!(res.headers()["content-type"], "text/markdown");
383
384 let body = res.into_body().collect().await.unwrap().to_bytes();
385 let body = String::from_utf8(body.to_vec()).unwrap();
386
387 assert!(body.starts_with("# Tower Async HTTP"));
388 }
389
390 #[tokio::test]
391 async fn fallbacks_to_different_precompressed_variant_if_not_found() {
392 let svc = ServeFile::new("./test-files/precompressed_br.txt")
393 .precompressed_gzip()
394 .precompressed_deflate()
395 .precompressed_br();
396
397 let request = Request::builder()
398 .header("Accept-Encoding", "gzip,deflate,br")
399 .body(Body::empty())
400 .unwrap();
401 let res = svc.oneshot(request).await.unwrap();
402
403 assert_eq!(res.headers()["content-type"], "text/plain");
404 assert_eq!(res.headers()["content-encoding"], "br");
405
406 let body = res.into_body().collect().await.unwrap().to_bytes();
407 let mut decompressed = Vec::new();
408 BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
409 let decompressed = String::from_utf8(decompressed.to_vec()).unwrap();
410 assert!(decompressed.starts_with("Test file"));
411 }
412
413 #[tokio::test]
414 async fn fallbacks_to_different_precompressed_variant_if_not_found_head_request() {
415 let svc = ServeFile::new("./test-files/precompressed_br.txt")
416 .precompressed_gzip()
417 .precompressed_deflate()
418 .precompressed_br();
419
420 let request = Request::builder()
421 .header("Accept-Encoding", "gzip,deflate,br")
422 .method(Method::HEAD)
423 .body(Body::empty())
424 .unwrap();
425 let res = svc.oneshot(request).await.unwrap();
426
427 assert_eq!(res.headers()["content-type"], "text/plain");
428 assert_eq!(res.headers()["content-length"], "15");
429 assert_eq!(res.headers()["content-encoding"], "br");
430
431 assert!(res.into_body().frame().await.is_none());
432 }
433
434 #[tokio::test]
435 async fn returns_404_if_file_doesnt_exist() {
436 let svc = ServeFile::new("../this-doesnt-exist.md");
437
438 let res = svc.oneshot(Request::new(Body::empty())).await.unwrap();
439
440 assert_eq!(res.status(), StatusCode::NOT_FOUND);
441 assert!(res.headers().get(header::CONTENT_TYPE).is_none());
442 }
443
444 #[tokio::test]
445 async fn returns_404_if_file_doesnt_exist_when_precompression_is_used() {
446 let svc = ServeFile::new("../this-doesnt-exist.md").precompressed_deflate();
447
448 let request = Request::builder()
449 .header("Accept-Encoding", "deflate")
450 .body(Body::empty())
451 .unwrap();
452 let res = svc.oneshot(request).await.unwrap();
453
454 assert_eq!(res.status(), StatusCode::NOT_FOUND);
455 assert!(res.headers().get(header::CONTENT_TYPE).is_none());
456 }
457
458 #[tokio::test]
459 async fn last_modified() {
460 let svc = ServeFile::new("../README.md");
461
462 let req = Request::builder().body(Body::empty()).unwrap();
463 let res = svc.oneshot(req).await.unwrap();
464
465 assert_eq!(res.status(), StatusCode::OK);
466
467 let last_modified = res
468 .headers()
469 .get(header::LAST_MODIFIED)
470 .expect("Missing last modified header!");
471
472 let svc = ServeFile::new("../README.md");
475 let req = Request::builder()
476 .header(header::IF_MODIFIED_SINCE, last_modified)
477 .body(Body::empty())
478 .unwrap();
479
480 let res = svc.oneshot(req).await.unwrap();
481 assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
482 assert!(res.into_body().frame().await.is_none());
483
484 let svc = ServeFile::new("../README.md");
485 let req = Request::builder()
486 .header(header::IF_MODIFIED_SINCE, "Fri, 09 Aug 1996 14:21:40 GMT")
487 .body(Body::empty())
488 .unwrap();
489
490 let res = svc.oneshot(req).await.unwrap();
491 assert_eq!(res.status(), StatusCode::OK);
492 let readme_bytes = include_bytes!("../../../../README.md");
493 let body = res.into_body().collect().await.unwrap().to_bytes();
494 assert_eq!(body.as_ref(), readme_bytes);
495
496 let svc = ServeFile::new("../README.md");
499 let req = Request::builder()
500 .header(header::IF_UNMODIFIED_SINCE, last_modified)
501 .body(Body::empty())
502 .unwrap();
503
504 let res = svc.oneshot(req).await.unwrap();
505 assert_eq!(res.status(), StatusCode::OK);
506 let body = res.into_body().collect().await.unwrap().to_bytes();
507 assert_eq!(body.as_ref(), readme_bytes);
508
509 let svc = ServeFile::new("../README.md");
510 let req = Request::builder()
511 .header(header::IF_UNMODIFIED_SINCE, "Fri, 09 Aug 1996 14:21:40 GMT")
512 .body(Body::empty())
513 .unwrap();
514
515 let res = svc.oneshot(req).await.unwrap();
516 assert_eq!(res.status(), StatusCode::PRECONDITION_FAILED);
517 assert!(res.into_body().frame().await.is_none());
518 }
519}