Skip to main content

tower_embed_core/
embedded.rs

1use 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
12/// A trait for types that embed a folder of assets.
13pub trait EmbedFolder {
14    /// Get an embedded asset by its relative path.
15    fn open(path: &str) -> impl Future<Output = std::io::Result<Embedded>> + Send + 'static;
16}
17
18/// An embedded binary asset.
19pub struct Embedded {
20    /// The content of the embedded asset.
21    pub content: Content,
22    /// The metadata associated with the embedded asset.
23    pub metadata: Metadata,
24}
25
26impl Embedded {
27    #[cfg(feature = "tokio")]
28    /// Loads a file dynamically from the filesystem.
29    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
56/// A stream of binary content.
57pub struct Content(BoxStream<'static, Result<Bytes, BoxError>>);
58
59impl Content {
60    /// Creates a [`Content`] from a static slice of bytes.
61    pub fn from_static(bytes: &'static [u8]) -> Self {
62        Self(Box::pin(StaticContent::new(bytes)))
63    }
64
65    /// Creates a [`Content`] from a stream of frames.
66    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/// Metadata associated with an embedded asset.
123#[derive(Clone, Debug)]
124pub struct Metadata {
125    /// MIME type of the resource.
126    pub content_type: headers::ContentType,
127    /// File unique identifier, to be used to match with `If-None-Match` header.
128    pub etag: Option<headers::ETag>,
129    /// The date and time when the resource was modified.
130    pub last_modified: Option<headers::LastModified>,
131}
132
133/// Returns the last modification time of file.
134pub 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
140/// Returns the MIME type of file.
141pub 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
148/// Returns the unique identifier tag of the content.
149pub 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
162/// Trait extension for converting embedded assets into HTTP responses.
163pub 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}