Skip to main content

trillium_static/
static_conn_ext.rs

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