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 }