trillium_static/
static_conn_ext.rs

1use crate::{fs_shims::File, options::StaticOptions};
2use etag::EntityTag;
3use std::path::Path;
4use trillium::{
5    Body, Conn,
6    KnownHeaderName::{self, ContentType},
7};
8
9/// conn extension trait to facilitate sending individual files and
10/// paths
11#[trillium::async_trait]
12pub trait StaticConnExt {
13    /// Send the file at the provided path. Will send a 404 if the
14    /// file cannot be resolved or if it is a directory.
15    async fn send_path<A: AsRef<Path> + Send>(self, path: A) -> Self;
16
17    /// Send the file at the provided path. Will send a 404 if the
18    /// file cannot be resolved or if it is a directory.
19    async fn send_file(self, file: File) -> Self;
20
21    /// Send the file at the provided path. Will send a 404 if the
22    /// file cannot be resolved or if it is a directory.
23    async fn send_file_with_options(self, file: File, options: &StaticOptions) -> Self;
24
25    /// Send the file at the provided path. Will send a 404 if the
26    /// file cannot be resolved or if it is a directory.
27    async fn send_path_with_options<A: AsRef<Path> + Send>(
28        self,
29        path: A,
30        options: &StaticOptions,
31    ) -> Self;
32
33    /// Guess the mime type for this fs path using
34    /// [`mime_guess`](https://docs.rs/mime_guess/) and set the
35    /// content-type header
36    fn with_mime_from_path(self, path: impl AsRef<Path>) -> Self;
37}
38
39#[trillium::async_trait]
40impl StaticConnExt for Conn {
41    async fn send_path<A: AsRef<Path> + Send>(mut self, path: A) -> Self {
42        self.send_path_with_options(path, &StaticOptions::default())
43            .await
44    }
45
46    async fn send_file(mut self, file: File) -> Self {
47        self.send_file_with_options(file, &StaticOptions::default())
48            .await
49    }
50
51    async fn send_path_with_options<A: AsRef<Path> + Send>(
52        mut self,
53        path: A,
54        options: &StaticOptions,
55    ) -> Self {
56        let path = path.as_ref().to_path_buf();
57        let file = trillium::conn_try!(File::open(&path).await, self.with_status(404));
58        self.send_file_with_options(file, options)
59            .await
60            .with_mime_from_path(path)
61    }
62
63    async fn send_file_with_options(mut self, file: File, options: &StaticOptions) -> Self {
64        let metadata = trillium::conn_try!(file.metadata().await, self.with_status(404));
65
66        if options.modified {
67            if let Ok(last_modified) = metadata.modified() {
68                self.response_headers_mut().try_insert(
69                    KnownHeaderName::LastModified,
70                    httpdate::fmt_http_date(last_modified),
71                );
72            }
73        }
74
75        if options.etag {
76            let etag = EntityTag::from_file_meta(&metadata);
77            self.response_headers_mut()
78                .try_insert(KnownHeaderName::Etag, etag.to_string());
79        }
80
81        #[cfg(feature = "tokio")]
82        let file = async_compat::Compat::new(file);
83
84        self.ok(Body::new_streaming(file, Some(metadata.len())))
85    }
86
87    fn with_mime_from_path(self, path: impl AsRef<Path>) -> Self {
88        if let Some(mime) = mime_guess::from_path(path).first() {
89            use mime_guess::mime::{APPLICATION, HTML, JAVASCRIPT, TEXT};
90            let is_text = matches!(
91                (mime.type_(), mime.subtype()),
92                (APPLICATION, JAVASCRIPT) | (TEXT, _) | (_, HTML)
93            );
94
95            self.with_response_header(
96                ContentType,
97                if is_text {
98                    format!("{mime}; charset=utf-8")
99                } else {
100                    mime.to_string()
101                },
102            )
103        } else {
104            self
105        }
106    }
107}