Skip to main content

trillium_static/
static_conn_ext.rs

1use crate::{ResolvedDirectory, 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    /// If [`StaticFileHandler`][crate::StaticFileHandler] resolved this
42    /// request's path to a directory but did not serve a file from it (no
43    /// index file configured, or the configured index was absent), returns the
44    /// resolved directory. A downstream handler can use this to render a
45    /// directory listing.
46    fn resolved_directory(&self) -> Option<&ResolvedDirectory>;
47}
48
49impl StaticConnExt for Conn {
50    async fn send_path<A: AsRef<Path> + Send>(self, path: A) -> Self {
51        self.send_path_with_options(path, &StaticOptions::default())
52            .await
53    }
54
55    async fn send_file(self, file: File) -> Self {
56        self.send_file_with_options(file, &StaticOptions::default())
57            .await
58    }
59
60    async fn send_path_with_options<A: AsRef<Path> + Send>(
61        self,
62        path: A,
63        options: &StaticOptions,
64    ) -> Self {
65        let path = path.as_ref().to_path_buf();
66        let file = trillium::conn_try!(File::open(&path).await, self.with_status(404));
67        self.send_file_with_options(file, options)
68            .await
69            .with_mime_from_path(path)
70    }
71
72    async fn send_file_with_options(mut self, file: File, options: &StaticOptions) -> Self {
73        let metadata = trillium::conn_try!(file.metadata().await, self.with_status(404));
74
75        if options.modified
76            && let Ok(last_modified) = metadata.modified()
77        {
78            self.response_headers_mut().try_insert(
79                KnownHeaderName::LastModified,
80                httpdate::fmt_http_date(last_modified),
81            );
82        }
83
84        if options.etag {
85            let etag = EntityTag::from_file_meta(&metadata);
86            self.response_headers_mut()
87                .try_insert(KnownHeaderName::Etag, etag.to_string());
88        }
89
90        #[cfg(feature = "tokio")]
91        let file = async_compat::Compat::new(file);
92
93        self.ok(Body::new_streaming(file, Some(metadata.len())))
94    }
95
96    fn resolved_directory(&self) -> Option<&ResolvedDirectory> {
97        self.state()
98    }
99
100    fn with_mime_from_path(self, path: impl AsRef<Path>) -> Self {
101        if let Some(mime) = mime_guess::from_path(path).first() {
102            use mime_guess::mime::{APPLICATION, HTML, JAVASCRIPT, TEXT};
103            let is_text = matches!(
104                (mime.type_(), mime.subtype()),
105                (APPLICATION, JAVASCRIPT) | (TEXT, _) | (_, HTML)
106            );
107
108            self.with_response_header(
109                ContentType,
110                if is_text {
111                    format!("{mime}; charset=utf-8")
112                } else {
113                    mime.to_string()
114                },
115            )
116        } else {
117            self
118        }
119    }
120}