trillium_static/
static_conn_ext.rs1use 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
9pub trait StaticConnExt: Send {
12 fn send_path<A: AsRef<Path> + Send>(self, path: A) -> impl Future<Output = Conn> + Send;
15
16 fn send_file(self, file: File) -> impl Future<Output = Conn> + Send;
19
20 fn send_file_with_options(
23 self,
24 file: File,
25 options: &StaticOptions,
26 ) -> impl Future<Output = Conn> + Send;
27
28 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 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}