1use super::{ServeDir, ServeDirSymlinkPolicy};
4use crate::mime::{Mime, guess as mime_guess};
5use crate::{Request, Response};
6use rama_core::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()
22 .unwrap_or(crate::mime::APPLICATION_OCTET_STREAM);
23 Self(ServeDir::new_single_file(path, mime))
24 }
25
26 pub fn new_with_mime<P: AsRef<Path>>(path: P, mime: Mime) -> Self {
34 Self(ServeDir::new_single_file(path, mime))
35 }
36
37 rama_utils::macros::generate_set_and_with! {
38 pub fn precompressed_gzip(mut self) -> Self {
49 self.0.set_precompressed_gzip();
50 self
51 }
52 }
53
54 rama_utils::macros::generate_set_and_with! {
55 pub fn precompressed_br(mut self) -> Self {
66 self.0.set_precompressed_br();
67 self
68 }
69 }
70
71 rama_utils::macros::generate_set_and_with! {
72 pub fn precompressed_deflate(mut self) -> Self {
83 self.0.set_precompressed_deflate();
84 self
85 }
86 }
87
88 rama_utils::macros::generate_set_and_with! {
89 pub fn precompressed_zstd(mut self) -> Self {
100 self.0.set_precompressed_zstd();
101 self
102 }
103 }
104
105 rama_utils::macros::generate_set_and_with! {
106 pub fn buf_chunk_size(mut self, chunk_size: usize) -> Self {
110 self.0.set_buf_chunk_size(chunk_size);
111 self
112 }
113 }
114
115 rama_utils::macros::generate_set_and_with! {
116 pub fn symlink_policy(mut self, policy: ServeDirSymlinkPolicy) -> Self {
120 self.0.set_symlink_policy(policy);
121 self
122 }
123 }
124
125 #[inline]
130 pub async fn try_call<ReqBody>(&self, req: Request<ReqBody>) -> Result<Response, std::io::Error>
131 where
132 ReqBody: Send + 'static,
133 {
134 self.0.try_call(req).await
135 }
136}
137
138impl<ReqBody> Service<Request<ReqBody>> for ServeFile
139where
140 ReqBody: Send + 'static,
141{
142 type Error = <ServeDir as Service<Request<ReqBody>>>::Error;
143 type Output = <ServeDir as Service<Request<ReqBody>>>::Output;
144
145 #[inline]
146 async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
147 self.0.serve(req).await
148 }
149}
150
151#[cfg(test)]
152#[cfg(feature = "compression")]
153mod compression_tests {
154 use super::*;
155 use crate::Body;
156
157 #[tokio::test]
158 #[cfg(feature = "compression")]
159 async fn precompressed_zstd() {
160 use crate::body::util::BodyExt;
161 use async_compression::tokio::bufread::ZstdDecoder;
162 use tokio::io::AsyncReadExt;
163
164 let svc = ServeFile::new("../test-files/precompressed.txt").with_precompressed_zstd();
165 let request = Request::builder()
166 .header("Accept-Encoding", "zstd,br")
167 .body(Body::empty())
168 .unwrap();
169 let res = svc.serve(request).await.unwrap();
170
171 assert_eq!(res.headers()["content-type"], "text/plain");
172 assert_eq!(res.headers()["content-encoding"], "zstd");
173
174 let body = res.into_body().collect().await.unwrap().to_bytes();
175 let mut decoder = ZstdDecoder::new(&body[..]);
176 let mut decompressed = String::new();
177 decoder.read_to_string(&mut decompressed).await.unwrap();
178 assert!(decompressed.starts_with("Test file"));
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use crate::Method;
185 use crate::header;
186 use crate::service::fs::ServeFile;
187 use crate::{Body, body::util::BodyExt};
188 use crate::{Request, StatusCode};
189 use brotli::BrotliDecompress;
190 use flate2::bufread::DeflateDecoder;
191 use flate2::bufread::GzDecoder;
192 use rama_core::Service;
193 use rama_http_types::mime::guess::mime;
194 use std::io::Read;
195
196 #[tokio::test]
197 async fn basic() {
198 let svc = ServeFile::new("../README.md");
199
200 let res = svc.serve(Request::new(Body::empty())).await.unwrap();
201
202 assert_eq!(res.headers()["content-type"], "text/markdown");
203
204 let body = res.into_body().collect().await.unwrap().to_bytes();
205 let body = String::from_utf8(body.to_vec()).unwrap();
206
207 assert!(body.starts_with("[![rama banner]"));
208 }
209
210 #[tokio::test]
211 async fn basic_with_mime() {
212 let svc = ServeFile::new_with_mime("../README.md", mime::IMAGE_JPEG);
213
214 let res = svc.serve(Request::new(Body::empty())).await.unwrap();
215
216 assert_eq!(res.headers()["content-type"], "image/jpeg");
217
218 let body = res.into_body().collect().await.unwrap().to_bytes();
219 let body = String::from_utf8(body.to_vec()).unwrap();
220
221 assert!(body.starts_with("[![rama banner]"));
222 }
223
224 #[tokio::test]
225 async fn head_request() {
226 let svc = ServeFile::new("../test-files/precompressed.txt");
227
228 let mut request = Request::new(Body::empty());
229 *request.method_mut() = Method::HEAD;
230 let res = svc.serve(request).await.unwrap();
231
232 assert_eq!(res.headers()["content-type"], "text/plain");
233
234 #[cfg(target_os = "windows")]
235 assert_eq!(res.headers()["content-length"], "11");
236 #[cfg(not(target_os = "windows"))]
237 assert_eq!(res.headers()["content-length"], "10");
238
239 assert!(res.into_body().frame().await.is_none());
240 }
241
242 #[tokio::test]
243 async fn precompresed_head_request() {
244 let svc = ServeFile::new("../test-files/precompressed.txt").with_precompressed_gzip();
245
246 let request = Request::builder()
247 .header("Accept-Encoding", "gzip")
248 .method(Method::HEAD)
249 .body(Body::empty())
250 .unwrap();
251 let res = svc.serve(request).await.unwrap();
252
253 assert_eq!(res.headers()["content-type"], "text/plain");
254 assert_eq!(res.headers()["content-encoding"], "gzip");
255 assert_eq!(res.headers()["content-length"], "30");
256
257 assert!(res.into_body().frame().await.is_none());
258 }
259
260 #[tokio::test]
261 async fn precompressed_gzip() {
262 let svc = ServeFile::new("../test-files/precompressed.txt").with_precompressed_gzip();
263
264 let request = Request::builder()
265 .header("Accept-Encoding", "gzip")
266 .body(Body::empty())
267 .unwrap();
268 let res = svc.serve(request).await.unwrap();
269
270 assert_eq!(res.headers()["content-type"], "text/plain");
271 assert_eq!(res.headers()["content-encoding"], "gzip");
272
273 let body = res.into_body().collect().await.unwrap().to_bytes();
274 let mut decoder = GzDecoder::new(&body[..]);
275 let mut decompressed = String::new();
276 decoder.read_to_string(&mut decompressed).unwrap();
277 assert!(decompressed.starts_with("Test file"));
278 }
279
280 #[tokio::test]
281 async fn unsupported_precompression_algorithm_fallbacks_to_uncompressed() {
282 let svc = ServeFile::new("../test-files/precompressed.txt").with_precompressed_gzip();
283
284 let request = Request::builder()
285 .header("Accept-Encoding", "br")
286 .body(Body::empty())
287 .unwrap();
288 let res = svc.serve(request).await.unwrap();
289
290 assert_eq!(res.headers()["content-type"], "text/plain");
291 assert!(res.headers().get("content-encoding").is_none());
292
293 let body = res.into_body().collect().await.unwrap().to_bytes();
294 let body = String::from_utf8(body.to_vec()).unwrap();
295 assert!(body.starts_with("Test file"));
296 }
297
298 #[tokio::test]
299 async fn missing_precompressed_variant_fallbacks_to_uncompressed() {
300 let svc =
301 ServeFile::new("../test-files/missing_precompressed.txt").with_precompressed_gzip();
302
303 let request = Request::builder()
304 .header("Accept-Encoding", "gzip")
305 .body(Body::empty())
306 .unwrap();
307 let res = svc.serve(request).await.unwrap();
308
309 assert_eq!(res.headers()["content-type"], "text/plain");
310 assert!(res.headers().get("content-encoding").is_none());
312
313 let body = res.into_body().collect().await.unwrap().to_bytes();
314 let body = String::from_utf8(body.to_vec()).unwrap();
315 assert!(body.starts_with("Test file"));
316 }
317
318 #[tokio::test]
319 async fn missing_precompressed_variant_fallbacks_to_uncompressed_head_request() {
320 let svc =
321 ServeFile::new("../test-files/missing_precompressed.txt").with_precompressed_gzip();
322
323 let request = Request::builder()
324 .header("Accept-Encoding", "gzip")
325 .method(Method::HEAD)
326 .body(Body::empty())
327 .unwrap();
328 let res = svc.serve(request).await.unwrap();
329
330 assert_eq!(res.headers()["content-type"], "text/plain");
331 #[cfg(target_os = "windows")]
332 assert_eq!(res.headers()["content-length"], "11");
333 #[cfg(not(target_os = "windows"))]
334 assert_eq!(res.headers()["content-length"], "10");
335 assert!(res.headers().get("content-encoding").is_none());
337
338 assert!(res.into_body().frame().await.is_none());
339 }
340
341 #[tokio::test]
342 async fn only_precompressed_variant_existing() {
343 let svc = ServeFile::new("../test-files/only_gzipped.txt").with_precompressed_gzip();
344
345 let request = Request::builder().body(Body::empty()).unwrap();
346 let res = svc.clone().serve(request).await.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(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("Test file"));
365 }
366
367 #[tokio::test]
368 async fn precompressed_br() {
369 let svc = ServeFile::new("../test-files/precompressed.txt").with_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(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.clone()).unwrap();
384 assert!(decompressed.starts_with("Test file"));
385 }
386
387 #[tokio::test]
388 async fn precompressed_deflate() {
389 let svc = ServeFile::new("../test-files/precompressed.txt").with_precompressed_deflate();
390 let request = Request::builder()
391 .header("Accept-Encoding", "deflate,br")
392 .body(Body::empty())
393 .unwrap();
394 let res = svc.serve(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("Test file"));
404 }
405
406 #[tokio::test]
407 async fn multi_precompressed() {
408 let svc = ServeFile::new("../test-files/precompressed.txt")
409 .with_precompressed_gzip()
410 .with_precompressed_br();
411
412 let request = Request::builder()
413 .header("Accept-Encoding", "gzip")
414 .body(Body::empty())
415 .unwrap();
416 let res = svc.clone().serve(request).await.unwrap();
417
418 assert_eq!(res.headers()["content-type"], "text/plain");
419 assert_eq!(res.headers()["content-encoding"], "gzip");
420
421 let body = res.into_body().collect().await.unwrap().to_bytes();
422 let mut decoder = GzDecoder::new(&body[..]);
423 let mut decompressed = String::new();
424 decoder.read_to_string(&mut decompressed).unwrap();
425 assert!(decompressed.starts_with("Test file"));
426
427 let request = Request::builder()
428 .header("Accept-Encoding", "br")
429 .body(Body::empty())
430 .unwrap();
431 let res = svc.clone().serve(request).await.unwrap();
432
433 assert_eq!(res.headers()["content-type"], "text/plain");
434 assert_eq!(res.headers()["content-encoding"], "br");
435
436 let body = res.into_body().collect().await.unwrap().to_bytes();
437 let mut decompressed = Vec::new();
438 BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
439 let decompressed = String::from_utf8(decompressed.clone()).unwrap();
440 assert!(decompressed.starts_with("Test file"));
441 }
442
443 #[tokio::test]
444 async fn with_custom_chunk_size() {
445 let svc = ServeFile::new("../README.md").with_buf_chunk_size(1024 * 32);
446
447 let res = svc.serve(Request::new(Body::empty())).await.unwrap();
448
449 assert_eq!(res.headers()["content-type"], "text/markdown");
450
451 let body = res.into_body().collect().await.unwrap().to_bytes();
452 let body = String::from_utf8(body.to_vec()).unwrap();
453
454 assert!(body.starts_with("[![rama banner]"));
455 }
456
457 #[tokio::test]
458 async fn fallbacks_to_different_precompressed_variant_if_not_found() {
459 let svc = ServeFile::new("../test-files/precompressed_br.txt")
460 .with_precompressed_gzip()
461 .with_precompressed_deflate()
462 .with_precompressed_br();
463
464 let request = Request::builder()
465 .header("Accept-Encoding", "gzip,deflate,br")
466 .body(Body::empty())
467 .unwrap();
468 let res = svc.serve(request).await.unwrap();
469
470 assert_eq!(res.headers()["content-type"], "text/plain");
471 assert_eq!(res.headers()["content-encoding"], "br");
472
473 let body = res.into_body().collect().await.unwrap().to_bytes();
474 let mut decompressed = Vec::new();
475 BrotliDecompress(&mut &body[..], &mut decompressed).unwrap();
476 let decompressed = String::from_utf8(decompressed.clone()).unwrap();
477 assert!(decompressed.starts_with("Test file"));
478 }
479
480 #[tokio::test]
481 async fn fallbacks_to_different_precompressed_variant_if_not_found_head_request() {
482 let svc = ServeFile::new("../test-files/precompressed_br.txt")
483 .with_precompressed_gzip()
484 .with_precompressed_deflate()
485 .with_precompressed_br();
486
487 let request = Request::builder()
488 .header("Accept-Encoding", "gzip,deflate,br")
489 .method(Method::HEAD)
490 .body(Body::empty())
491 .unwrap();
492 let res = svc.serve(request).await.unwrap();
493
494 assert_eq!(res.headers()["content-type"], "text/plain");
495 assert_eq!(res.headers()["content-length"], "15");
496 assert_eq!(res.headers()["content-encoding"], "br");
497
498 assert!(res.into_body().frame().await.is_none());
499 }
500
501 #[tokio::test]
502 async fn returns_404_if_file_doesnt_exist() {
503 let svc = ServeFile::new("../this-doesnt-exist.md");
504
505 let res = svc.serve(Request::new(Body::empty())).await.unwrap();
506
507 assert_eq!(res.status(), StatusCode::NOT_FOUND);
508 assert!(res.headers().get(header::CONTENT_TYPE).is_none());
509 }
510
511 #[tokio::test]
512 async fn returns_404_if_file_doesnt_exist_when_precompression_is_used() {
513 let svc = ServeFile::new("../this-doesnt-exist.md").with_precompressed_deflate();
514
515 let request = Request::builder()
516 .header("Accept-Encoding", "deflate")
517 .body(Body::empty())
518 .unwrap();
519 let res = svc.serve(request).await.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 last_modified() {
527 let svc = ServeFile::new("../README.md");
528
529 let req = Request::builder().body(Body::empty()).unwrap();
530 let res = svc.serve(req).await.unwrap();
531
532 assert_eq!(res.status(), StatusCode::OK);
533
534 let last_modified = res
535 .headers()
536 .get(header::LAST_MODIFIED)
537 .expect("Missing last modified header!");
538
539 let svc = ServeFile::new("../README.md");
542 let req = Request::builder()
543 .header(header::IF_MODIFIED_SINCE, last_modified)
544 .body(Body::empty())
545 .unwrap();
546
547 let res = svc.serve(req).await.unwrap();
548 assert_eq!(res.status(), StatusCode::NOT_MODIFIED);
549 assert!(res.into_body().frame().await.is_none());
550
551 let svc = ServeFile::new("../README.md");
552 let req = Request::builder()
553 .header(header::IF_MODIFIED_SINCE, "Fri, 09 Aug 1996 14:21:40 GMT")
554 .body(Body::empty())
555 .unwrap();
556
557 let res = svc.serve(req).await.unwrap();
558 assert_eq!(res.status(), StatusCode::OK);
559 let readme_bytes = include_bytes!("../../../../README.md");
560 let body = res.into_body().collect().await.unwrap().to_bytes();
561 assert_eq!(body.as_ref(), readme_bytes);
562
563 let svc = ServeFile::new("../README.md");
566 let req = Request::builder()
567 .header(header::IF_UNMODIFIED_SINCE, last_modified)
568 .body(Body::empty())
569 .unwrap();
570
571 let res = svc.serve(req).await.unwrap();
572 assert_eq!(res.status(), StatusCode::OK);
573 let body = res.into_body().collect().await.unwrap().to_bytes();
574 assert_eq!(body.as_ref(), readme_bytes);
575
576 let svc = ServeFile::new("../README.md");
577 let req = Request::builder()
578 .header(header::IF_UNMODIFIED_SINCE, "Fri, 09 Aug 1996 14:21:40 GMT")
579 .body(Body::empty())
580 .unwrap();
581
582 let res = svc.serve(req).await.unwrap();
583 assert_eq!(res.status(), StatusCode::PRECONDITION_FAILED);
584 assert!(res.into_body().frame().await.is_none());
585 }
586}