tower_async_http/services/fs/
serve_file.rs

1//! Service that serves a file.
2
3use super::ServeDir;
4use http::{HeaderValue, Request, Response};
5use mime::Mime;
6use std::path::Path;
7use tower_async_service::Service;
8
9/// Service that serves a file.
10#[derive(Clone, Debug)]
11pub struct ServeFile(ServeDir);
12
13// Note that this is just a special case of ServeDir
14impl ServeFile {
15    /// Create a new [`ServeFile`].
16    ///
17    /// The `Content-Type` will be guessed from the file extension.
18    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    /// Create a new [`ServeFile`] with a specific mime type.
31    ///
32    /// # Panics
33    ///
34    /// Will panic if the mime type isn't a valid [header value].
35    ///
36    /// [header value]: https://docs.rs/http/latest/http/header/struct.HeaderValue.html
37    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    /// Informs the service that it should also look for a precompressed gzip
43    /// version of the file.
44    ///
45    /// If the client has an `Accept-Encoding` header that allows the gzip encoding,
46    /// the file `foo.txt.gz` will be served instead of `foo.txt`.
47    /// If the precompressed file is not available, or the client doesn't support it,
48    /// the uncompressed version will be served instead.
49    /// Both the precompressed version and the uncompressed version are expected
50    /// to be present in the same directory. Different precompressed
51    /// variants can be combined.
52    pub fn precompressed_gzip(self) -> Self {
53        Self(self.0.precompressed_gzip())
54    }
55
56    /// Informs the service that it should also look for a precompressed brotli
57    /// version of the file.
58    ///
59    /// If the client has an `Accept-Encoding` header that allows the brotli encoding,
60    /// the file `foo.txt.br` will be served instead of `foo.txt`.
61    /// If the precompressed file is not available, or the client doesn't support it,
62    /// the uncompressed version will be served instead.
63    /// Both the precompressed version and the uncompressed version are expected
64    /// to be present in the same directory. Different precompressed
65    /// variants can be combined.
66    pub fn precompressed_br(self) -> Self {
67        Self(self.0.precompressed_br())
68    }
69
70    /// Informs the service that it should also look for a precompressed deflate
71    /// version of the file.
72    ///
73    /// If the client has an `Accept-Encoding` header that allows the deflate encoding,
74    /// the file `foo.txt.zz` will be served instead of `foo.txt`.
75    /// If the precompressed file is not available, or the client doesn't support it,
76    /// the uncompressed version will be served instead.
77    /// Both the precompressed version and the uncompressed version are expected
78    /// to be present in the same directory. Different precompressed
79    /// variants can be combined.
80    pub fn precompressed_deflate(self) -> Self {
81        Self(self.0.precompressed_deflate())
82    }
83
84    /// Set a specific read buffer chunk size.
85    ///
86    /// The default capacity is 64kb.
87    pub fn with_buf_chunk_size(self, chunk_size: usize) -> Self {
88        Self(self.0.with_buf_chunk_size(chunk_size))
89    }
90
91    /// Call the service and get a future that contains any `std::io::Error` that might have
92    /// happened.
93    ///
94    /// See [`ServeDir::try_call`] for more details.
95    #[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        // Uncompressed file is served because compressed version is missing
248        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        // Uncompressed file is served because compressed version is missing
269        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        // Should reply with gzipped file if client supports it
284        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        // -- If-Modified-Since
473
474        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        // -- If-Unmodified-Since
497
498        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}