tower_embed_core/
embedded.rs1use std::{
2 pin::Pin,
3 task::{Context, Poll, ready},
4};
5
6use bytes::Bytes;
7use futures_core::{Stream, stream::BoxStream};
8use http_body::Frame;
9
10use crate::{Body, BoxError, headers, response};
11
12pub trait EmbedFolder {
14 fn open(path: &str) -> impl Future<Output = std::io::Result<Embedded>> + Send + 'static;
16}
17
18pub struct Embedded {
20 pub content: Content,
22 pub metadata: Metadata,
24}
25
26impl Embedded {
27 #[cfg(feature = "tokio")]
28 pub async fn load_file(
30 path: String,
31 root: &'static str,
32 index: &'static str,
33 ) -> std::io::Result<Embedded> {
34 use std::path::Path;
35
36 let mut filename = Path::new(root).join(&path);
37 let stripped_path = Path::new(root).join(path.trim_end_matches('/'));
38 if stripped_path.is_dir() {
39 filename = filename.join(index);
40 }
41
42 let metadata = Metadata {
43 content_type: crate::content_type(&filename),
44 etag: None,
45 last_modified: None,
46 };
47
48 let file = crate::file::File::open(&filename).await?;
49 Ok(Embedded {
50 content: Content::from_stream(file),
51 metadata,
52 })
53 }
54}
55
56pub struct Content(BoxStream<'static, Result<Bytes, BoxError>>);
58
59impl Content {
60 pub fn from_static(bytes: &'static [u8]) -> Self {
62 Self(Box::pin(StaticContent::new(bytes)))
63 }
64
65 pub fn from_stream<S, E>(stream: S) -> Self
67 where
68 S: Stream<Item = Result<Bytes, E>> + Send + 'static,
69 E: Into<BoxError>,
70 {
71 Self(Box::pin(StreamContent(stream)))
72 }
73}
74
75impl Stream for Content {
76 type Item = Result<Frame<Bytes>, BoxError>;
77
78 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
79 self.0.as_mut().poll_next(cx).map_ok(Frame::data)
80 }
81}
82
83struct StaticContent(Option<&'static [u8]>);
84
85impl StaticContent {
86 pub fn new(bytes: &'static [u8]) -> Self {
87 Self(Some(bytes))
88 }
89}
90
91impl Stream for StaticContent {
92 type Item = Result<Bytes, BoxError>;
93
94 fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
95 self.0
96 .take()
97 .map(|bytes| Ok(Bytes::from_static(bytes)))
98 .into()
99 }
100}
101
102struct StreamContent<S>(S);
103
104impl<S, E> Stream for StreamContent<S>
105where
106 S: Stream<Item = Result<Bytes, E>>,
107 E: Into<BoxError>,
108{
109 type Item = Result<Bytes, BoxError>;
110
111 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
112 let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
113 match ready!(inner.poll_next(cx)) {
114 Some(Ok(bytes)) => Some(Ok(bytes)),
115 Some(Err(err)) => Some(Err(err.into())),
116 None => None,
117 }
118 .into()
119 }
120}
121
122#[derive(Clone, Debug)]
124pub struct Metadata {
125 pub content_type: headers::ContentType,
127 pub etag: Option<headers::ETag>,
129 pub last_modified: Option<headers::LastModified>,
131}
132
133pub fn last_modified(path: &std::path::Path) -> std::io::Result<headers::LastModified> {
135 std::fs::metadata(path)
136 .and_then(|metadata| metadata.modified())
137 .map(headers::LastModified)
138}
139
140pub fn content_type(path: &std::path::Path) -> headers::ContentType {
142 mime_guess::from_path(path)
143 .first()
144 .map(headers::ContentType)
145 .unwrap_or_else(headers::ContentType::octet_stream)
146}
147
148pub fn etag(content: &[u8]) -> headers::ETag {
150 use std::hash::Hasher;
151
152 let hash: u64 = {
153 let mut hasher = rapidhash::fast::RapidHasher::default_const();
154 hasher.write(content);
155 hasher.finish()
156 };
157
158 let etag = format!("{:016x}", hash);
159 headers::ETag::new(&etag).unwrap()
160}
161
162pub trait EmbeddedExt {
164 fn into_response(self, req: http::Request<()>) -> http::Response<Body>;
165}
166
167impl EmbeddedExt for Embedded {
168 fn into_response(self, req: http::Request<()>) -> http::Response<Body> {
169 use crate::headers::{self, HeaderMapExt};
170
171 let if_none_match = req.headers().typed_get::<headers::IfNoneMatch>();
172 if let Some(if_none_match) = if_none_match
173 && let Some(etag) = &self.metadata.etag
174 && !if_none_match.condition_passes(etag)
175 {
176 return crate::response::not_modified();
177 }
178
179 let if_modified_since = req.headers().typed_get::<headers::IfModifiedSince>();
180 if let Some(if_modified_since) = if_modified_since
181 && let Some(last_modified) = &self.metadata.last_modified
182 && !if_modified_since.condition_passes(last_modified)
183 {
184 return crate::response::not_modified();
185 }
186
187 let mut response = http::Response::builder()
188 .status(http::StatusCode::OK)
189 .body(Body::stream(self.content))
190 .unwrap();
191
192 response
193 .headers_mut()
194 .typed_insert(self.metadata.content_type);
195 if let Some(etag) = self.metadata.etag {
196 response.headers_mut().typed_insert(etag);
197 }
198 if let Some(last_modified) = self.metadata.last_modified {
199 response.headers_mut().typed_insert(last_modified);
200 }
201
202 response
203 }
204}
205
206impl EmbeddedExt for std::io::Result<Embedded> {
207 fn into_response(self, req: http::Request<()>) -> http::Response<Body> {
208 match self {
209 Ok(embedded) => embedded.into_response(req),
210 Err(err)
211 if err.kind() == std::io::ErrorKind::NotFound
212 || err.kind() == std::io::ErrorKind::NotADirectory =>
213 {
214 response::not_found()
215 }
216 Err(_) => response::internal_server_error(),
217 }
218 }
219}