Skip to main content

rama_http/service/fs/
serve_file.rs

1//! Service that serves a file.
2
3use super::{ServeDir, ServeDirSymlinkPolicy};
4use crate::mime::{Mime, guess as mime_guess};
5use crate::{Request, Response};
6use rama_core::Service;
7use std::path::Path;
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()
22            .unwrap_or(crate::mime::APPLICATION_OCTET_STREAM);
23        Self(ServeDir::new_single_file(path, mime))
24    }
25
26    /// Create a new [`ServeFile`] with a specific mime type.
27    ///
28    /// # Panics
29    ///
30    /// Will panic if the mime type isn't a valid [header value].
31    ///
32    /// [header value]: https://docs.rs/http/latest/http/header/struct.HeaderValue.html
33    pub fn new_with_mime<P: AsRef<Path>>(path: P, mime: Mime) -> Self {
34        Self(ServeDir::new_single_file(path, mime))
35    }
36
37    rama_utils::macros::generate_set_and_with! {
38        /// Informs the service that it should also look for a precompressed gzip
39        /// version of the file.
40        ///
41        /// If the client has an `Accept-Encoding` header that allows the gzip encoding,
42        /// the file `foo.txt.gz` will be served instead of `foo.txt`.
43        /// If the precompressed file is not available, or the client doesn't support it,
44        /// the uncompressed version will be served instead.
45        /// Both the precompressed version and the uncompressed version are expected
46        /// to be present in the same directory. Different precompressed
47        /// variants can be combined.
48        pub fn precompressed_gzip(mut self) -> Self {
49            self.0.set_precompressed_gzip();
50            self
51        }
52    }
53
54    rama_utils::macros::generate_set_and_with! {
55        /// Informs the service that it should also look for a precompressed brotli
56        /// version of the file.
57        ///
58        /// If the client has an `Accept-Encoding` header that allows the brotli encoding,
59        /// the file `foo.txt.br` will be served instead of `foo.txt`.
60        /// If the precompressed file is not available, or the client doesn't support it,
61        /// the uncompressed version will be served instead.
62        /// Both the precompressed version and the uncompressed version are expected
63        /// to be present in the same directory. Different precompressed
64        /// variants can be combined.
65        pub fn precompressed_br(mut self) -> Self {
66            self.0.set_precompressed_br();
67            self
68        }
69    }
70
71    rama_utils::macros::generate_set_and_with! {
72        /// Informs the service that it should also look for a precompressed deflate
73        /// version of the file.
74        ///
75        /// If the client has an `Accept-Encoding` header that allows the deflate encoding,
76        /// the file `foo.txt.zz` will be served instead of `foo.txt`.
77        /// If the precompressed file is not available, or the client doesn't support it,
78        /// the uncompressed version will be served instead.
79        /// Both the precompressed version and the uncompressed version are expected
80        /// to be present in the same directory. Different precompressed
81        /// variants can be combined.
82        pub fn precompressed_deflate(mut self) -> Self {
83            self.0.set_precompressed_deflate();
84            self
85        }
86    }
87
88    rama_utils::macros::generate_set_and_with! {
89        /// Informs the service that it should also look for a precompressed zstd
90        /// version of the file.
91        ///
92        /// If the client has an `Accept-Encoding` header that allows the zstd encoding,
93        /// the file `foo.txt.zst` will be served instead of `foo.txt`.
94        /// If the precompressed file is not available, or the client doesn't support it,
95        /// the uncompressed version will be served instead.
96        /// Both the precompressed version and the uncompressed version are expected
97        /// to be present in the same directory. Different precompressed
98        /// variants can be combined.
99        pub fn precompressed_zstd(mut self) -> Self {
100            self.0.set_precompressed_zstd();
101            self
102        }
103    }
104
105    rama_utils::macros::generate_set_and_with! {
106        /// Set a specific read buffer chunk size.
107        ///
108        /// The default capacity is 64kb.
109        pub fn buf_chunk_size(mut self, chunk_size: usize) -> Self {
110            self.0.set_buf_chunk_size(chunk_size);
111            self
112        }
113    }
114
115    rama_utils::macros::generate_set_and_with! {
116        /// Set the filesystem symlink policy.
117        ///
118        /// Defaults to [`ServeDirSymlinkPolicy::RejectAll`].
119        pub fn symlink_policy(mut self, policy: ServeDirSymlinkPolicy) -> Self {
120            self.0.set_symlink_policy(policy);
121            self
122        }
123    }
124
125    /// Call the service and get a future that contains any `std::io::Error` that might have
126    /// happened.
127    ///
128    /// See [`ServeDir::try_call`] for more details.
129    #[inline]
130    pub async fn try_call<ReqBody>(&self, req: Request<ReqBody>) -> Result<Response, std::io::Error>
131    where
132        ReqBody: Send + 'static,
133    {
134        self.0.try_call(req).await
135    }
136}
137
138impl<ReqBody> Service<Request<ReqBody>> for ServeFile
139where
140    ReqBody: Send + 'static,
141{
142    type Error = <ServeDir as Service<Request<ReqBody>>>::Error;
143    type Output = <ServeDir as Service<Request<ReqBody>>>::Output;
144
145    #[inline]
146    async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
147        self.0.serve(req).await
148    }
149}
150
151#[cfg(test)]
152#[cfg(feature = "compression")]
153mod compression_tests {
154    use super::*;
155    use crate::Body;
156
157    #[tokio::test]
158    #[cfg(feature = "compression")]
159    async fn precompressed_zstd() {
160        use crate::body::util::BodyExt;
161        use async_compression::tokio::bufread::ZstdDecoder;
162        use tokio::io::AsyncReadExt;
163
164        let svc = ServeFile::new("../test-files/precompressed.txt").with_precompressed_zstd();
165        let request = Request::builder()
166            .header("Accept-Encoding", "zstd,br")
167            .body(Body::empty())
168            .unwrap();
169        let res = svc.serve(request).await.unwrap();
170
171        assert_eq!(res.headers()["content-type"], "text/plain");
172        assert_eq!(res.headers()["content-encoding"], "zstd");
173
174        let body = res.into_body().collect().await.unwrap().to_bytes();
175        let mut decoder = ZstdDecoder::new(&body[..]);
176        let mut decompressed = String::new();
177        decoder.read_to_string(&mut decompressed).await.unwrap();
178        assert!(decompressed.starts_with("Test file"));
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use crate::Method;
185    use crate::header;
186    use crate::service::fs::ServeFile;
187    use crate::{Body, body::util::BodyExt};
188    use crate::{Request, StatusCode};
189    use brotli::BrotliDecompress;
190    use flate2::bufread::DeflateDecoder;
191    use flate2::bufread::GzDecoder;
192    use rama_core::Service;
193    use rama_http_types::mime::guess::mime;
194    use std::io::Read;
195
196    #[tokio::test]
197    async fn basic() {
198        let svc = ServeFile::new("../README.md");
199
200        let res = svc.serve(Request::new(Body::empty())).await.unwrap();
201
202        assert_eq!(res.headers()["content-type"], "text/markdown");
203
204        let body = res.into_body().collect().await.unwrap().to_bytes();
205        let body = String::from_utf8(body.to_vec()).unwrap();
206
207        assert!(body.starts_with("[![rama banner]"));
208    }
209
210    #[tokio::test]
211    async fn basic_with_mime() {
212        let svc = ServeFile::new_with_mime("../README.md", mime::IMAGE_JPEG);
213
214        let res = svc.serve(Request::new(Body::empty())).await.unwrap();
215
216        assert_eq!(res.headers()["content-type"], "image/jpeg");
217
218        let body = res.into_body().collect().await.unwrap().to_bytes();
219        let body = String::from_utf8(body.to_vec()).unwrap();
220
221        assert!(body.starts_with("[![rama banner]"));
222    }
223
224    #[tokio::test]
225    async fn head_request() {
226        let svc = ServeFile::new("../test-files/precompressed.txt");
227
228        let mut request = Request::new(Body::empty());
229        *request.method_mut() = Method::HEAD;
230        let res = svc.serve(request).await.unwrap();
231
232        assert_eq!(res.headers()["content-type"], "text/plain");
233
234        #[cfg(target_os = "windows")]
235        assert_eq!(res.headers()["content-length"], "11");
236        #[cfg(not(target_os = "windows"))]
237        assert_eq!(res.headers()["content-length"], "10");
238
239        assert!(res.into_body().frame().await.is_none());
240    }
241
242    #[tokio::test]
243    async fn precompresed_head_request() {
244        let svc = ServeFile::new("../test-files/precompressed.txt").with_precompressed_gzip();
245
246        let request = Request::builder()
247            .header("Accept-Encoding", "gzip")
248            .method(Method::HEAD)
249            .body(Body::empty())
250            .unwrap();
251        let res = svc.serve(request).await.unwrap();
252
253        assert_eq!(res.headers()["content-type"], "text/plain");
254        assert_eq!(res.headers()["content-encoding"], "gzip");
255        assert_eq!(res.headers()["content-length"], "30");
256
257        assert!(res.into_body().frame().await.is_none());
258    }
259
260    #[tokio::test]
261    async fn precompressed_gzip() {
262        let svc = ServeFile::new("../test-files/precompressed.txt").with_precompressed_gzip();
263
264        let request = Request::builder()
265            .header("Accept-Encoding", "gzip")
266            .body(Body::empty())
267            .unwrap();
268        let res = svc.serve(request).await.unwrap();
269
270        assert_eq!(res.headers()["content-type"], "text/plain");
271        assert_eq!(res.headers()["content-encoding"], "gzip");
272
273        let body = res.into_body().collect().await.unwrap().to_bytes();
274        let mut decoder = GzDecoder::new(&body[..]);
275        let mut decompressed = String::new();
276        decoder.read_to_string(&mut decompressed).unwrap();
277        assert!(decompressed.starts_with("Test file"));
278    }
279
280    #[tokio::test]
281    async fn unsupported_precompression_algorithm_fallbacks_to_uncompressed() {
282        let svc = ServeFile::new("../test-files/precompressed.txt").with_precompressed_gzip();
283
284        let request = Request::builder()
285            .header("Accept-Encoding", "br")
286            .body(Body::empty())
287            .unwrap();
288        let res = svc.serve(request).await.unwrap();
289
290        assert_eq!(res.headers()["content-type"], "text/plain");
291        assert!(res.headers().get("content-encoding").is_none());
292
293        let body = res.into_body().collect().await.unwrap().to_bytes();
294        let body = String::from_utf8(body.to_vec()).unwrap();
295        assert!(body.starts_with("Test file"));
296    }
297
298    #[tokio::test]
299    async fn missing_precompressed_variant_fallbacks_to_uncompressed() {
300        let svc =
301            ServeFile::new("../test-files/missing_precompressed.txt").with_precompressed_gzip();
302
303        let request = Request::builder()
304            .header("Accept-Encoding", "gzip")
305            .body(Body::empty())
306            .unwrap();
307        let res = svc.serve(request).await.unwrap();
308
309        assert_eq!(res.headers()["content-type"], "text/plain");
310        // Uncompressed file is served because compressed version is missing
311        assert!(res.headers().get("content-encoding").is_none());
312
313        let body = res.into_body().collect().await.unwrap().to_bytes();
314        let body = String::from_utf8(body.to_vec()).unwrap();
315        assert!(body.starts_with("Test file"));
316    }
317
318    #[tokio::test]
319    async fn missing_precompressed_variant_fallbacks_to_uncompressed_head_request() {
320        let svc =
321            ServeFile::new("../test-files/missing_precompressed.txt").with_precompressed_gzip();
322
323        let request = Request::builder()
324            .header("Accept-Encoding", "gzip")
325            .method(Method::HEAD)
326            .body(Body::empty())
327            .unwrap();
328        let res = svc.serve(request).await.unwrap();
329
330        assert_eq!(res.headers()["content-type"], "text/plain");
331        #[cfg(target_os = "windows")]
332        assert_eq!(res.headers()["content-length"], "11");
333        #[cfg(not(target_os = "windows"))]
334        assert_eq!(res.headers()["content-length"], "10");
335        // Uncompressed file is served because compressed version is missing
336        assert!(res.headers().get("content-encoding").is_none());
337
338        assert!(res.into_body().frame().await.is_none());
339    }
340
341    #[tokio::test]
342    async fn only_precompressed_variant_existing() {
343        let svc = ServeFile::new("../test-files/only_gzipped.txt").with_precompressed_gzip();
344
345        let request = Request::builder().body(Body::empty()).unwrap();
346        let res = svc.clone().serve(request).await.unwrap();
347
348        assert_eq!(res.status(), StatusCode::NOT_FOUND);
349
350        // Should reply with gzipped file if client supports it
351        let request = Request::builder()
352            .header("Accept-Encoding", "gzip")
353            .body(Body::empty())
354            .unwrap();
355        let res = svc.serve(request).await.unwrap();
356
357        assert_eq!(res.headers()["content-type"], "text/plain");
358        assert_eq!(res.headers()["content-encoding"], "gzip");
359
360        let body = res.into_body().collect().await.unwrap().to_bytes();
361        let mut decoder = GzDecoder::new(&body[..]);
362        let mut decompressed = String::new();
363        decoder.read_to_string(&mut decompressed).unwrap();
364        assert!(decompressed.starts_with("Test file"));
365    }
366
367    #[tokio::test]
368    async fn precompressed_br() {
369        let svc = ServeFile::new("../test-files/precompressed.txt").with_precompressed_br();
370
371        let request = Request::builder()
372            .header("Accept-Encoding", "gzip,br")
373            .body(Body::empty())
374            .unwrap();
375        let res = svc.serve(request).await.unwrap();
376
377        assert_eq!(res.headers()["content-type"], "text/plain");
378        assert_eq!(res.headers()["content-encoding"], "br");
379
380        let body = res.into_body().collect().await.unwrap().to_bytes();
381        let mut decompressed = Vec::new();
382        BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
383        let decompressed = String::from_utf8(decompressed.clone()).unwrap();
384        assert!(decompressed.starts_with("Test file"));
385    }
386
387    #[tokio::test]
388    async fn precompressed_deflate() {
389        let svc = ServeFile::new("../test-files/precompressed.txt").with_precompressed_deflate();
390        let request = Request::builder()
391            .header("Accept-Encoding", "deflate,br")
392            .body(Body::empty())
393            .unwrap();
394        let res = svc.serve(request).await.unwrap();
395
396        assert_eq!(res.headers()["content-type"], "text/plain");
397        assert_eq!(res.headers()["content-encoding"], "deflate");
398
399        let body = res.into_body().collect().await.unwrap().to_bytes();
400        let mut decoder = DeflateDecoder::new(&body[..]);
401        let mut decompressed = String::new();
402        decoder.read_to_string(&mut decompressed).unwrap();
403        assert!(decompressed.starts_with("Test file"));
404    }
405
406    #[tokio::test]
407    async fn multi_precompressed() {
408        let svc = ServeFile::new("../test-files/precompressed.txt")
409            .with_precompressed_gzip()
410            .with_precompressed_br();
411
412        let request = Request::builder()
413            .header("Accept-Encoding", "gzip")
414            .body(Body::empty())
415            .unwrap();
416        let res = svc.clone().serve(request).await.unwrap();
417
418        assert_eq!(res.headers()["content-type"], "text/plain");
419        assert_eq!(res.headers()["content-encoding"], "gzip");
420
421        let body = res.into_body().collect().await.unwrap().to_bytes();
422        let mut decoder = GzDecoder::new(&body[..]);
423        let mut decompressed = String::new();
424        decoder.read_to_string(&mut decompressed).unwrap();
425        assert!(decompressed.starts_with("Test file"));
426
427        let request = Request::builder()
428            .header("Accept-Encoding", "br")
429            .body(Body::empty())
430            .unwrap();
431        let res = svc.clone().serve(request).await.unwrap();
432
433        assert_eq!(res.headers()["content-type"], "text/plain");
434        assert_eq!(res.headers()["content-encoding"], "br");
435
436        let body = res.into_body().collect().await.unwrap().to_bytes();
437        let mut decompressed = Vec::new();
438        BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
439        let decompressed = String::from_utf8(decompressed.clone()).unwrap();
440        assert!(decompressed.starts_with("Test file"));
441    }
442
443    #[tokio::test]
444    async fn with_custom_chunk_size() {
445        let svc = ServeFile::new("../README.md").with_buf_chunk_size(1024 * 32);
446
447        let res = svc.serve(Request::new(Body::empty())).await.unwrap();
448
449        assert_eq!(res.headers()["content-type"], "text/markdown");
450
451        let body = res.into_body().collect().await.unwrap().to_bytes();
452        let body = String::from_utf8(body.to_vec()).unwrap();
453
454        assert!(body.starts_with("[![rama banner]"));
455    }
456
457    #[tokio::test]
458    async fn fallbacks_to_different_precompressed_variant_if_not_found() {
459        let svc = ServeFile::new("../test-files/precompressed_br.txt")
460            .with_precompressed_gzip()
461            .with_precompressed_deflate()
462            .with_precompressed_br();
463
464        let request = Request::builder()
465            .header("Accept-Encoding", "gzip,deflate,br")
466            .body(Body::empty())
467            .unwrap();
468        let res = svc.serve(request).await.unwrap();
469
470        assert_eq!(res.headers()["content-type"], "text/plain");
471        assert_eq!(res.headers()["content-encoding"], "br");
472
473        let body = res.into_body().collect().await.unwrap().to_bytes();
474        let mut decompressed = Vec::new();
475        BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
476        let decompressed = String::from_utf8(decompressed.clone()).unwrap();
477        assert!(decompressed.starts_with("Test file"));
478    }
479
480    #[tokio::test]
481    async fn fallbacks_to_different_precompressed_variant_if_not_found_head_request() {
482        let svc = ServeFile::new("../test-files/precompressed_br.txt")
483            .with_precompressed_gzip()
484            .with_precompressed_deflate()
485            .with_precompressed_br();
486
487        let request = Request::builder()
488            .header("Accept-Encoding", "gzip,deflate,br")
489            .method(Method::HEAD)
490            .body(Body::empty())
491            .unwrap();
492        let res = svc.serve(request).await.unwrap();
493
494        assert_eq!(res.headers()["content-type"], "text/plain");
495        assert_eq!(res.headers()["content-length"], "15");
496        assert_eq!(res.headers()["content-encoding"], "br");
497
498        assert!(res.into_body().frame().await.is_none());
499    }
500
501    #[tokio::test]
502    async fn returns_404_if_file_doesnt_exist() {
503        let svc = ServeFile::new("../this-doesnt-exist.md");
504
505        let res = svc.serve(Request::new(Body::empty())).await.unwrap();
506
507        assert_eq!(res.status(), StatusCode::NOT_FOUND);
508        assert!(res.headers().get(header::CONTENT_TYPE).is_none());
509    }
510
511    #[tokio::test]
512    async fn returns_404_if_file_doesnt_exist_when_precompression_is_used() {
513        let svc = ServeFile::new("../this-doesnt-exist.md").with_precompressed_deflate();
514
515        let request = Request::builder()
516            .header("Accept-Encoding", "deflate")
517            .body(Body::empty())
518            .unwrap();
519        let res = svc.serve(request).await.unwrap();
520
521        assert_eq!(res.status(), StatusCode::NOT_FOUND);
522        assert!(res.headers().get(header::CONTENT_TYPE).is_none());
523    }
524
525    #[tokio::test]
526    async fn last_modified() {
527        let svc = ServeFile::new("../README.md");
528
529        let req = Request::builder().body(Body::empty()).unwrap();
530        let res = svc.serve(req).await.unwrap();
531
532        assert_eq!(res.status(), StatusCode::OK);
533
534        let last_modified = res
535            .headers()
536            .get(header::LAST_MODIFIED)
537            .expect("Missing last modified header!");
538
539        // -- If-Modified-Since
540
541        let svc = ServeFile::new("../README.md");
542        let req = Request::builder()
543            .header(header::IF_MODIFIED_SINCE, last_modified)
544            .body(Body::empty())
545            .unwrap();
546
547        let res = svc.serve(req).await.unwrap();
548        assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
549        assert!(res.into_body().frame().await.is_none());
550
551        let svc = ServeFile::new("../README.md");
552        let req = Request::builder()
553            .header(header::IF_MODIFIED_SINCE, "Fri, 09 Aug 1996 14:21:40 GMT")
554            .body(Body::empty())
555            .unwrap();
556
557        let res = svc.serve(req).await.unwrap();
558        assert_eq!(res.status(), StatusCode::OK);
559        let readme_bytes = include_bytes!("../../../../README.md");
560        let body = res.into_body().collect().await.unwrap().to_bytes();
561        assert_eq!(body.as_ref(), readme_bytes);
562
563        // -- If-Unmodified-Since
564
565        let svc = ServeFile::new("../README.md");
566        let req = Request::builder()
567            .header(header::IF_UNMODIFIED_SINCE, last_modified)
568            .body(Body::empty())
569            .unwrap();
570
571        let res = svc.serve(req).await.unwrap();
572        assert_eq!(res.status(), StatusCode::OK);
573        let body = res.into_body().collect().await.unwrap().to_bytes();
574        assert_eq!(body.as_ref(), readme_bytes);
575
576        let svc = ServeFile::new("../README.md");
577        let req = Request::builder()
578            .header(header::IF_UNMODIFIED_SINCE, "Fri, 09 Aug 1996 14:21:40 GMT")
579            .body(Body::empty())
580            .unwrap();
581
582        let res = svc.serve(req).await.unwrap();
583        assert_eq!(res.status(), StatusCode::PRECONDITION_FAILED);
584        assert!(res.into_body().frame().await.is_none());
585    }
586}