trillium_static/
static_conn_ext.rs1use 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
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 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}