1use std::{
17 borrow::Cow,
18 marker::PhantomData,
19 task::{Context, Poll},
20};
21
22#[doc(no_inline)]
23pub use rust_embed;
24
25use rust_embed::RustEmbed;
26use tower_service::Service;
27
28use self::headers::HeaderMapExt;
29
30#[doc(inline)]
31pub use self::response::{ResponseBody, ResponseFuture};
32
33mod headers;
34mod response;
35
36pub struct ServeEmbed<E: RustEmbed> {
38 _embed: PhantomData<E>,
39}
40
41impl<E: RustEmbed> Clone for ServeEmbed<E> {
42 fn clone(&self) -> Self {
43 *self
44 }
45}
46
47impl<E: RustEmbed> Copy for ServeEmbed<E> {}
48
49impl<E: RustEmbed> ServeEmbed<E> {
50 pub fn new() -> Self {
52 Self {
53 _embed: PhantomData,
54 }
55 }
56}
57
58impl<E: RustEmbed> Default for ServeEmbed<E> {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64impl<E, ReqBody> Service<http::Request<ReqBody>> for ServeEmbed<E>
65where
66 E: RustEmbed,
67{
68 type Response = http::Response<ResponseBody>;
69 type Error = std::convert::Infallible;
70 type Future = ResponseFuture;
71
72 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
73 Poll::Ready(Ok(()))
74 }
75
76 fn call(&mut self, req: http::Request<ReqBody>) -> Self::Future {
77 if req.method() != http::Method::GET && req.method() != http::Method::HEAD {
78 return ResponseFuture::method_not_allowed();
79 }
80
81 let path = get_file_path_from_uri(req.uri());
82 let Some(file) = E::get(path.as_ref()) else {
83 return ResponseFuture::file_not_found();
84 };
85
86 let content_type = file.metadata.content_type_header();
88 let etag = file.metadata.etag_header();
89 let last_modified = file.metadata.last_modified_header();
90
91 if let Some(if_none_match) = req.headers().typed_get::<headers::IfNoneMatch>()
93 && !if_none_match.condition_passes(&etag)
94 {
95 return ResponseFuture::not_modified();
96 }
97
98 if let Some(if_modified_since) = req.headers().typed_get::<headers::IfModifiedSince>()
100 && let Some(last_modified) = last_modified
101 && !if_modified_since.condition_passes(&last_modified)
102 {
103 return ResponseFuture::not_modified();
104 }
105
106 ResponseFuture::file(response::File {
107 content: file.data.clone(),
108 content_type,
109 etag,
110 last_modified,
111 })
112 }
113}
114
115fn get_file_path_from_uri(uri: &http::Uri) -> Cow<'_, str> {
116 let path = uri.path();
117 if path.ends_with("/") {
118 Cow::Owned(format!("{}index.html", path.trim_start_matches('/')))
119 } else {
120 Cow::Borrowed(path.trim_start_matches('/'))
121 }
122}
123
124trait MetadataExt {
125 fn etag_header(&self) -> headers::ETag;
127
128 fn content_type_header(&self) -> headers::ContentType;
130
131 fn last_modified_header(&self) -> Option<headers::LastModified>;
133}
134
135impl MetadataExt for rust_embed::Metadata {
136 fn etag_header(&self) -> headers::ETag {
137 let etag = self
138 .sha256_hash()
139 .iter()
140 .map(|b| format!("{b:02x}"))
141 .collect::<String>();
142 headers::ETag::new(&etag).unwrap()
143 }
144
145 fn content_type_header(&self) -> headers::ContentType {
146 headers::ContentType::from_str(self.mimetype())
147 .unwrap_or_else(headers::ContentType::octet_stream)
148 }
149
150 fn last_modified_header(&self) -> Option<headers::LastModified> {
151 let unix_timestamp = self.last_modified()?;
152 headers::LastModified::from_unix_timestamp(unix_timestamp)
153 }
154}