trillium_static/
static_conn_ext.rs1use 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#[trillium::async_trait]
12pub trait StaticConnExt {
13 async fn send_path<A: AsRef<Path> + Send>(self, path: A) -> Self;
16
17 async fn send_file(self, file: File) -> Self;
20
21 async fn send_file_with_options(self, file: File, options: &StaticOptions) -> Self;
24
25 async fn send_path_with_options<A: AsRef<Path> + Send>(
28 self,
29 path: A,
30 options: &StaticOptions,
31 ) -> Self;
32
33 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}