1use super::ServeDir;
4use crate::dep::{mime::Mime, mime_guess};
5use crate::{HeaderValue, Request, Response};
6use rama_core::{Context, Service};
7use std::path::Path;
8
9#[derive(Clone, Debug)]
11pub struct ServeFile(ServeDir);
12
13impl ServeFile {
15 pub fn new<P: AsRef<Path>>(path: P) -> Self {
19 let guess = mime_guess::from_path(path.as_ref());
20 let mime = guess
21 .first_raw()
22 .map(HeaderValue::from_static)
23 .unwrap_or_else(|| {
24 HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap()
25 });
26
27 ServeFile(ServeDir::new_single_file(path, mime))
28 }
29
30 pub fn new_with_mime<P: AsRef<Path>>(path: P, mime: &Mime) -> Self {
38 let mime = HeaderValue::from_str(mime.as_ref()).expect("mime isn't a valid header value");
39 ServeFile(ServeDir::new_single_file(path, mime))
40 }
41
42 pub fn precompressed_gzip(self) -> Self {
53 Self(self.0.precompressed_gzip())
54 }
55
56 pub fn precompressed_br(self) -> Self {
67 Self(self.0.precompressed_br())
68 }
69
70 pub fn precompressed_deflate(self) -> Self {
81 Self(self.0.precompressed_deflate())
82 }
83
84 pub fn precompressed_zstd(self) -> Self {
95 Self(self.0.precompressed_zstd())
96 }
97
98 pub fn with_buf_chunk_size(self, chunk_size: usize) -> Self {
102 Self(self.0.with_buf_chunk_size(chunk_size))
103 }
104
105 #[inline]
110 pub async fn try_call<State, ReqBody>(
111 &self,
112 ctx: Context<State>,
113 req: Request<ReqBody>,
114 ) -> Result<Response, std::io::Error>
115 where
116 State: Clone + Send + Sync + 'static,
117 ReqBody: Send + 'static,
118 {
119 self.0.try_call(ctx, req).await
120 }
121}
122
123impl<State, ReqBody> Service<State, Request<ReqBody>> for ServeFile
124where
125 ReqBody: Send + 'static,
126 State: Clone + Send + Sync + 'static,
127{
128 type Error = <ServeDir as Service<State, Request<ReqBody>>>::Error;
129 type Response = <ServeDir as Service<State, Request<ReqBody>>>::Response;
130
131 #[inline]
132 async fn serve(
133 &self,
134 ctx: Context<State>,
135 req: Request<ReqBody>,
136 ) -> Result<Self::Response, Self::Error> {
137 self.0.serve(ctx, req).await
138 }
139}
140
141#[cfg(test)]
142#[cfg(feature = "compression")]
143mod compression_tests {
144 use super::*;
145 use crate::Body;
146
147 #[tokio::test]
148 #[cfg(feature = "compression")]
149 async fn precompressed_zstd() {
150 use async_compression::tokio::bufread::ZstdDecoder;
151 use rama_http_types::dep::http_body_util::BodyExt;
152 use tokio::io::AsyncReadExt;
153
154 let svc = ServeFile::new("../test-files/precompressed.txt").precompressed_zstd();
155 let request = Request::builder()
156 .header("Accept-Encoding", "zstd,br")
157 .body(Body::empty())
158 .unwrap();
159 let res = svc.serve(Context::default(), request).await.unwrap();
160
161 assert_eq!(res.headers()["content-type"], "text/plain");
162 assert_eq!(res.headers()["content-encoding"], "zstd");
163
164 let body = res.into_body().collect().await.unwrap().to_bytes();
165 let mut decoder = ZstdDecoder::new(&body[..]);
166 let mut decompressed = String::new();
167 decoder.read_to_string(&mut decompressed).await.unwrap();
168 assert!(decompressed.starts_with("\"This is a test file!\""));
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use crate::Body;
175 use crate::Method;
176 use crate::dep::http_body_util::BodyExt;
177 use crate::dep::mime::Mime;
178 use crate::header;
179 use crate::service::fs::ServeFile;
180 use crate::{Request, StatusCode};
181 use brotli::BrotliDecompress;
182 use flate2::bufread::DeflateDecoder;
183 use flate2::bufread::GzDecoder;
184 use rama_core::{Context, Service};
185 use std::io::Read;
186 use std::str::FromStr;
187
188 #[tokio::test]
189 async fn basic() {
190 let svc = ServeFile::new("../README.md");
191
192 let res = svc
193 .serve(Context::default(), Request::new(Body::empty()))
194 .await
195 .unwrap();
196
197 assert_eq!(res.headers()["content-type"], "text/markdown");
198
199 let body = res.into_body().collect().await.unwrap().to_bytes();
200 let body = String::from_utf8(body.to_vec()).unwrap();
201
202 assert!(body.starts_with("[![rama banner]"));
203 }
204
205 #[tokio::test]
206 async fn basic_with_mime() {
207 let svc = ServeFile::new_with_mime("../README.md", &Mime::from_str("image/jpg").unwrap());
208
209 let res = svc
210 .serve(Context::default(), Request::new(Body::empty()))
211 .await
212 .unwrap();
213
214 assert_eq!(res.headers()["content-type"], "image/jpg");
215
216 let body = res.into_body().collect().await.unwrap().to_bytes();
217 let body = String::from_utf8(body.to_vec()).unwrap();
218
219 assert!(body.starts_with("[![rama banner]"));
220 }
221
222 #[tokio::test]
223 async fn head_request() {
224 let svc = ServeFile::new("../test-files/precompressed.txt");
225
226 let mut request = Request::new(Body::empty());
227 *request.method_mut() = Method::HEAD;
228 let res = svc.serve(Context::default(), request).await.unwrap();
229
230 assert_eq!(res.headers()["content-type"], "text/plain");
231
232 #[cfg(target_os = "windows")]
233 assert_eq!(res.headers()["content-length"], "24");
234 #[cfg(not(target_os = "windows"))]
235 assert_eq!(res.headers()["content-length"], "23");
236
237 assert!(res.into_body().frame().await.is_none());
238 }
239
240 #[tokio::test]
241 async fn precompresed_head_request() {
242 let svc = ServeFile::new("../test-files/precompressed.txt").precompressed_gzip();
243
244 let request = Request::builder()
245 .header("Accept-Encoding", "gzip")
246 .method(Method::HEAD)
247 .body(Body::empty())
248 .unwrap();
249 let res = svc.serve(Context::default(), request).await.unwrap();
250
251 assert_eq!(res.headers()["content-type"], "text/plain");
252 assert_eq!(res.headers()["content-encoding"], "gzip");
253 assert_eq!(res.headers()["content-length"], "59");
254
255 assert!(res.into_body().frame().await.is_none());
256 }
257
258 #[tokio::test]
259 async fn precompressed_gzip() {
260 let svc = ServeFile::new("../test-files/precompressed.txt").precompressed_gzip();
261
262 let request = Request::builder()
263 .header("Accept-Encoding", "gzip")
264 .body(Body::empty())
265 .unwrap();
266 let res = svc.serve(Context::default(), request).await.unwrap();
267
268 assert_eq!(res.headers()["content-type"], "text/plain");
269 assert_eq!(res.headers()["content-encoding"], "gzip");
270
271 let body = res.into_body().collect().await.unwrap().to_bytes();
272 let mut decoder = GzDecoder::new(&body[..]);
273 let mut decompressed = String::new();
274 decoder.read_to_string(&mut decompressed).unwrap();
275 assert!(decompressed.starts_with("\"This is a test file!\""));
276 }
277
278 #[tokio::test]
279 async fn unsupported_precompression_algorithm_fallbacks_to_uncompressed() {
280 let svc = ServeFile::new("../test-files/precompressed.txt").precompressed_gzip();
281
282 let request = Request::builder()
283 .header("Accept-Encoding", "br")
284 .body(Body::empty())
285 .unwrap();
286 let res = svc.serve(Context::default(), request).await.unwrap();
287
288 assert_eq!(res.headers()["content-type"], "text/plain");
289 assert!(res.headers().get("content-encoding").is_none());
290
291 let body = res.into_body().collect().await.unwrap().to_bytes();
292 let body = String::from_utf8(body.to_vec()).unwrap();
293 assert!(body.starts_with("\"This is a test file!\""));
294 }
295
296 #[tokio::test]
297 async fn missing_precompressed_variant_fallbacks_to_uncompressed() {
298 let svc = ServeFile::new("../test-files/missing_precompressed.txt").precompressed_gzip();
299
300 let request = Request::builder()
301 .header("Accept-Encoding", "gzip")
302 .body(Body::empty())
303 .unwrap();
304 let res = svc.serve(Context::default(), request).await.unwrap();
305
306 assert_eq!(res.headers()["content-type"], "text/plain");
307 assert!(res.headers().get("content-encoding").is_none());
309
310 let body = res.into_body().collect().await.unwrap().to_bytes();
311 let body = String::from_utf8(body.to_vec()).unwrap();
312 assert!(body.starts_with("Test file!"));
313 }
314
315 #[tokio::test]
316 async fn missing_precompressed_variant_fallbacks_to_uncompressed_head_request() {
317 let svc = ServeFile::new("../test-files/missing_precompressed.txt").precompressed_gzip();
318
319 let request = Request::builder()
320 .header("Accept-Encoding", "gzip")
321 .method(Method::HEAD)
322 .body(Body::empty())
323 .unwrap();
324 let res = svc.serve(Context::default(), request).await.unwrap();
325
326 assert_eq!(res.headers()["content-type"], "text/plain");
327 #[cfg(target_os = "windows")]
328 assert_eq!(res.headers()["content-length"], "12");
329 #[cfg(not(target_os = "windows"))]
330 assert_eq!(res.headers()["content-length"], "11");
331 assert!(res.headers().get("content-encoding").is_none());
333
334 assert!(res.into_body().frame().await.is_none());
335 }
336
337 #[tokio::test]
338 async fn only_precompressed_variant_existing() {
339 let svc = ServeFile::new("../test-files/only_gzipped.txt").precompressed_gzip();
340
341 let request = Request::builder().body(Body::empty()).unwrap();
342 let res = svc
343 .clone()
344 .serve(Context::default(), request)
345 .await
346 .unwrap();
347
348 assert_eq!(res.status(), StatusCode::NOT_FOUND);
349
350 let request = Request::builder()
352 .header("Accept-Encoding", "gzip")
353 .body(Body::empty())
354 .unwrap();
355 let res = svc.serve(Context::default(), request).await.unwrap();
356
357 assert_eq!(res.headers()["content-type"], "text/plain");
358 assert_eq!(res.headers()["content-encoding"], "gzip");
359
360 let body = res.into_body().collect().await.unwrap().to_bytes();
361 let mut decoder = GzDecoder::new(&body[..]);
362 let mut decompressed = String::new();
363 decoder.read_to_string(&mut decompressed).unwrap();
364 assert!(decompressed.starts_with("\"This is a test file\""));
365 }
366
367 #[tokio::test]
368 async fn precompressed_br() {
369 let svc = ServeFile::new("../test-files/precompressed.txt").precompressed_br();
370
371 let request = Request::builder()
372 .header("Accept-Encoding", "gzip,br")
373 .body(Body::empty())
374 .unwrap();
375 let res = svc.serve(Context::default(), request).await.unwrap();
376
377 assert_eq!(res.headers()["content-type"], "text/plain");
378 assert_eq!(res.headers()["content-encoding"], "br");
379
380 let body = res.into_body().collect().await.unwrap().to_bytes();
381 let mut decompressed = Vec::new();
382 BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
383 let decompressed = String::from_utf8(decompressed.to_vec()).unwrap();
384 assert!(decompressed.starts_with("\"This is a test file!\""));
385 }
386
387 #[tokio::test]
388 async fn precompressed_deflate() {
389 let svc = ServeFile::new("../test-files/precompressed.txt").precompressed_deflate();
390 let request = Request::builder()
391 .header("Accept-Encoding", "deflate,br")
392 .body(Body::empty())
393 .unwrap();
394 let res = svc.serve(Context::default(), request).await.unwrap();
395
396 assert_eq!(res.headers()["content-type"], "text/plain");
397 assert_eq!(res.headers()["content-encoding"], "deflate");
398
399 let body = res.into_body().collect().await.unwrap().to_bytes();
400 let mut decoder = DeflateDecoder::new(&body[..]);
401 let mut decompressed = String::new();
402 decoder.read_to_string(&mut decompressed).unwrap();
403 assert!(decompressed.starts_with("\"This is a test file!\""));
404 }
405
406 #[tokio::test]
407 async fn multi_precompressed() {
408 let svc = ServeFile::new("../test-files/precompressed.txt")
409 .precompressed_gzip()
410 .precompressed_br();
411
412 let request = Request::builder()
413 .header("Accept-Encoding", "gzip")
414 .body(Body::empty())
415 .unwrap();
416 let res = svc
417 .clone()
418 .serve(Context::default(), request)
419 .await
420 .unwrap();
421
422 assert_eq!(res.headers()["content-type"], "text/plain");
423 assert_eq!(res.headers()["content-encoding"], "gzip");
424
425 let body = res.into_body().collect().await.unwrap().to_bytes();
426 let mut decoder = GzDecoder::new(&body[..]);
427 let mut decompressed = String::new();
428 decoder.read_to_string(&mut decompressed).unwrap();
429 assert!(decompressed.starts_with("\"This is a test file!\""));
430
431 let request = Request::builder()
432 .header("Accept-Encoding", "br")
433 .body(Body::empty())
434 .unwrap();
435 let res = svc
436 .clone()
437 .serve(Context::default(), request)
438 .await
439 .unwrap();
440
441 assert_eq!(res.headers()["content-type"], "text/plain");
442 assert_eq!(res.headers()["content-encoding"], "br");
443
444 let body = res.into_body().collect().await.unwrap().to_bytes();
445 let mut decompressed = Vec::new();
446 BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
447 let decompressed = String::from_utf8(decompressed.to_vec()).unwrap();
448 assert!(decompressed.starts_with("\"This is a test file!\""));
449 }
450
451 #[tokio::test]
452 async fn with_custom_chunk_size() {
453 let svc = ServeFile::new("../README.md").with_buf_chunk_size(1024 * 32);
454
455 let res = svc
456 .serve(Context::default(), Request::new(Body::empty()))
457 .await
458 .unwrap();
459
460 assert_eq!(res.headers()["content-type"], "text/markdown");
461
462 let body = res.into_body().collect().await.unwrap().to_bytes();
463 let body = String::from_utf8(body.to_vec()).unwrap();
464
465 assert!(body.starts_with("[![rama banner]"));
466 }
467
468 #[tokio::test]
469 async fn fallbacks_to_different_precompressed_variant_if_not_found() {
470 let svc = ServeFile::new("../test-files/precompressed_br.txt")
471 .precompressed_gzip()
472 .precompressed_deflate()
473 .precompressed_br();
474
475 let request = Request::builder()
476 .header("Accept-Encoding", "gzip,deflate,br")
477 .body(Body::empty())
478 .unwrap();
479 let res = svc.serve(Context::default(), request).await.unwrap();
480
481 assert_eq!(res.headers()["content-type"], "text/plain");
482 assert_eq!(res.headers()["content-encoding"], "br");
483
484 let body = res.into_body().collect().await.unwrap().to_bytes();
485 let mut decompressed = Vec::new();
486 BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
487 let decompressed = String::from_utf8(decompressed.to_vec()).unwrap();
488 assert!(decompressed.starts_with("Test file"));
489 }
490
491 #[tokio::test]
492 async fn fallbacks_to_different_precompressed_variant_if_not_found_head_request() {
493 let svc = ServeFile::new("../test-files/precompressed_br.txt")
494 .precompressed_gzip()
495 .precompressed_deflate()
496 .precompressed_br();
497
498 let request = Request::builder()
499 .header("Accept-Encoding", "gzip,deflate,br")
500 .method(Method::HEAD)
501 .body(Body::empty())
502 .unwrap();
503 let res = svc.serve(Context::default(), request).await.unwrap();
504
505 assert_eq!(res.headers()["content-type"], "text/plain");
506 assert_eq!(res.headers()["content-length"], "15");
507 assert_eq!(res.headers()["content-encoding"], "br");
508
509 assert!(res.into_body().frame().await.is_none());
510 }
511
512 #[tokio::test]
513 async fn returns_404_if_file_doesnt_exist() {
514 let svc = ServeFile::new("../this-doesnt-exist.md");
515
516 let res = svc
517 .serve(Context::default(), Request::new(Body::empty()))
518 .await
519 .unwrap();
520
521 assert_eq!(res.status(), StatusCode::NOT_FOUND);
522 assert!(res.headers().get(header::CONTENT_TYPE).is_none());
523 }
524
525 #[tokio::test]
526 async fn returns_404_if_file_doesnt_exist_when_precompression_is_used() {
527 let svc = ServeFile::new("../this-doesnt-exist.md").precompressed_deflate();
528
529 let request = Request::builder()
530 .header("Accept-Encoding", "deflate")
531 .body(Body::empty())
532 .unwrap();
533 let res = svc.serve(Context::default(), request).await.unwrap();
534
535 assert_eq!(res.status(), StatusCode::NOT_FOUND);
536 assert!(res.headers().get(header::CONTENT_TYPE).is_none());
537 }
538
539 #[tokio::test]
540 async fn last_modified() {
541 let svc = ServeFile::new("../README.md");
542
543 let req = Request::builder().body(Body::empty()).unwrap();
544 let res = svc.serve(Context::default(), req).await.unwrap();
545
546 assert_eq!(res.status(), StatusCode::OK);
547
548 let last_modified = res
549 .headers()
550 .get(header::LAST_MODIFIED)
551 .expect("Missing last modified header!");
552
553 let svc = ServeFile::new("../README.md");
556 let req = Request::builder()
557 .header(header::IF_MODIFIED_SINCE, last_modified)
558 .body(Body::empty())
559 .unwrap();
560
561 let res = svc.serve(Context::default(), req).await.unwrap();
562 assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
563 assert!(res.into_body().frame().await.is_none());
564
565 let svc = ServeFile::new("../README.md");
566 let req = Request::builder()
567 .header(header::IF_MODIFIED_SINCE, "Fri, 09 Aug 1996 14:21:40 GMT")
568 .body(Body::empty())
569 .unwrap();
570
571 let res = svc.serve(Context::default(), req).await.unwrap();
572 assert_eq!(res.status(), StatusCode::OK);
573 let readme_bytes = include_bytes!("../../../../README.md");
574 let body = res.into_body().collect().await.unwrap().to_bytes();
575 assert_eq!(body.as_ref(), readme_bytes);
576
577 let svc = ServeFile::new("../README.md");
580 let req = Request::builder()
581 .header(header::IF_UNMODIFIED_SINCE, last_modified)
582 .body(Body::empty())
583 .unwrap();
584
585 let res = svc.serve(Context::default(), req).await.unwrap();
586 assert_eq!(res.status(), StatusCode::OK);
587 let body = res.into_body().collect().await.unwrap().to_bytes();
588 assert_eq!(body.as_ref(), readme_bytes);
589
590 let svc = ServeFile::new("../README.md");
591 let req = Request::builder()
592 .header(header::IF_UNMODIFIED_SINCE, "Fri, 09 Aug 1996 14:21:40 GMT")
593 .body(Body::empty())
594 .unwrap();
595
596 let res = svc.serve(Context::default(), req).await.unwrap();
597 assert_eq!(res.status(), StatusCode::PRECONDITION_FAILED);
598 assert!(res.into_body().frame().await.is_none());
599 }
600}