tower_serve_static/
serve_file.rs1use super::{AsyncReadBody, DEFAULT_CAPACITY};
2use bytes::Bytes;
3use http::{header, HeaderValue, Response};
4use http_body::Frame;
5use http_body_util::{combinators::BoxBody, BodyExt};
6use std::{
7    convert::Infallible,
8    future::Future,
9    io,
10    pin::Pin,
11    task::{Context, Poll},
12};
13use tower_service::Service;
14
15#[derive(Clone, Debug)]
17pub struct File {
18    bytes: &'static [u8],
19    mime: HeaderValue,
20}
21
22impl File {
23    pub fn new(bytes: &'static [u8], mime: HeaderValue) -> Self {
25        File { bytes, mime }
26    }
27}
28
29#[macro_export]
33macro_rules! include_file {
34    ($file:expr) => {
35        $crate::File::new(
36            ::std::include_bytes!(::std::concat!(::std::env!("CARGO_MANIFEST_DIR"), $file)),
37            $crate::private::mime_guess::from_path(&$file)
38                .first_raw()
39                .map(|mime| $crate::private::http::HeaderValue::from_static(mime))
40                .unwrap_or_else(|| {
41                    $crate::private::http::HeaderValue::from_str(
42                        $crate::private::mime::APPLICATION_OCTET_STREAM.as_ref(),
43                    )
44                    .unwrap()
45                }),
46        )
47    };
48}
49
50#[macro_export]
58macro_rules! include_file_with_mime {
59    ($file:expr, $mime:expr) => {
60        $crate::File {
61            bytes: ::std::include_bytes!(::std::concat!(::std::env!("CARGO_MANIFEST_DIR"), $file)),
62            mime: $crate::private::http::HeaderValue::from_str($mime.as_ref())
63                .expect("mime isn't a valid header value"),
64        }
65    };
66}
67
68#[derive(Clone, Debug)]
70pub struct ServeFile {
71    file: File,
72    buf_chunk_size: usize,
73}
74
75impl ServeFile {
76    pub fn new(file: File) -> Self {
78        Self {
79            file,
80            buf_chunk_size: DEFAULT_CAPACITY,
81        }
82    }
83
84    pub fn with_buf_chunk_size(mut self, chunk_size: usize) -> Self {
88        self.buf_chunk_size = chunk_size;
89        self
90    }
91}
92
93impl<R> Service<R> for ServeFile {
94    type Response = Response<ResponseBody>;
95    type Error = Infallible;
96    type Future = ResponseFuture;
97
98    #[inline]
99    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
100        Poll::Ready(Ok(()))
101    }
102
103    fn call(&mut self, _req: R) -> Self::Future {
104        ResponseFuture {
105            file: Some(self.file.clone()),
106            buf_chunk_size: self.buf_chunk_size,
107        }
108    }
109}
110
111pub struct ResponseFuture {
113    file: Option<File>,
114    buf_chunk_size: usize,
115}
116
117impl Future for ResponseFuture {
118    type Output = Result<Response<ResponseBody>, Infallible>;
119
120    fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
121        let file = self.file.take().unwrap();
122
123        let chunk_size = self.buf_chunk_size;
124        let body = AsyncReadBody::with_capacity(file.bytes, chunk_size).boxed();
125        let body = ResponseBody(body);
126
127        let mut res = Response::new(body);
128        res.headers_mut().insert(header::CONTENT_TYPE, file.mime);
129
130        Poll::Ready(Ok(res))
131    }
132}
133
134opaque_body! {
135    pub type ResponseBody = BoxBody<Bytes, io::Error>;
137}
138
139#[cfg(test)]
140mod tests {
141    #[allow(unused_imports)]
142    use super::*;
143    use http::Request;
144    use tower::ServiceExt;
145
146    #[tokio::test]
147    async fn basic() {
148        let svc = ServeFile::new(include_file!("/README.md"));
149
150        let res = svc
151            .oneshot(Request::new(http_body_util::Empty::<Bytes>::new()))
152            .await
153            .unwrap();
154
155        assert_eq!(res.headers()["content-type"], "text/markdown");
156
157        let body = res.into_body().collect().await.unwrap();
158        let body = String::from_utf8(body.to_bytes().to_vec()).unwrap();
159
160        assert!(body.starts_with("# Tower Serve Static"));
161    }
162
163    #[tokio::test]
164    async fn with_custom_chunk_size() {
165        let svc = ServeFile::new(include_file!("/README.md")).with_buf_chunk_size(1024 * 32);
166
167        let res = svc
168            .oneshot(Request::new(http_body_util::Empty::<Bytes>::new()))
169            .await
170            .unwrap();
171
172        assert_eq!(res.headers()["content-type"], "text/markdown");
173
174        let body = res.into_body().collect().await.unwrap();
175        let body = String::from_utf8(body.to_bytes().to_vec()).unwrap();
176
177        assert!(body.starts_with("# Tower Serve Static"));
178    }
179
180    #[tokio::test]
181    async fn with_mime() {
182        let svc = ServeFile::new(include_file_with_mime!(
183            "/README.md",
184            mime::APPLICATION_OCTET_STREAM
185        ));
186
187        let res = svc
188            .oneshot(Request::new(http_body_util::Empty::<Bytes>::new()))
189            .await
190            .unwrap();
191
192        assert_eq!(res.headers()["content-type"], "application/octet-stream");
193
194        let body = res.into_body().collect().await.unwrap();
195        let body = String::from_utf8(body.to_bytes().to_vec()).unwrap();
196
197        assert!(body.starts_with("# Tower Serve Static"));
198    }
199
200    }