1use crate::headers::encoding::{SupportedEncodings, parse_accept_encoding_headers};
2use crate::layer::set_status::SetStatus;
3use crate::mime::Mime;
4use crate::service::web::response::IntoResponse;
5use crate::{Body, Method, Request, Response, StatusCode, StreamingBody, header};
6use rama_core::Service;
7use rama_core::bytes::Bytes;
8use rama_core::error::BoxError;
9use rama_core::error::BoxErrorExt as _;
10use rama_core::telemetry::tracing;
11use rama_net::uri::util::percent_encoding::percent_decode;
12use rama_utils::include_dir::Dir;
13use std::fmt;
14use std::str::FromStr;
15use std::{
16 convert::Infallible,
17 path::{Path, PathBuf},
18};
19
20pub(crate) mod future;
21mod headers;
22mod open_file;
23
24#[cfg(test)]
25mod tests;
26
27#[derive(Clone, Debug)]
29enum DirSource {
30 Filesystem(PathBuf),
31 Embedded(Dir<'static>),
32}
33
34const DEFAULT_CAPACITY: usize = 65536;
36
37#[derive(Clone, Debug)]
50pub struct ServeDir<F = DefaultServeDirFallback> {
51 base: DirSource,
52 buf_chunk_size: usize,
53 symlink_policy: ServeDirSymlinkPolicy,
54 precompressed_variants: Option<PrecompressedVariants>,
55 variant: ServeVariant,
58 fallback: Option<F>,
59 call_fallback_on_method_not_allowed: bool,
60}
61
62impl ServeDir<DefaultServeDirFallback> {
63 pub fn new<P>(path: P) -> Self
65 where
66 P: AsRef<Path>,
67 {
68 let mut base = PathBuf::from(".");
69 base.push(path.as_ref());
70
71 Self::new_with_base(DirSource::Filesystem(base))
72 }
73
74 #[must_use]
76 pub fn new_embedded(path: Dir<'static>) -> Self {
77 Self::new_with_base(DirSource::Embedded(path))
78 }
79
80 fn new_with_base(base: DirSource) -> Self {
82 Self {
83 base,
84 buf_chunk_size: DEFAULT_CAPACITY,
85 symlink_policy: ServeDirSymlinkPolicy::default(),
86 precompressed_variants: None,
87 variant: ServeVariant::Directory {
88 serve_mode: Default::default(),
89 html_as_default_extension: false,
90 },
91 fallback: None,
92 call_fallback_on_method_not_allowed: false,
93 }
94 }
95
96 pub(crate) fn new_single_file<P>(path: P, mime: Mime) -> Self
98 where
99 P: AsRef<Path>,
100 {
101 Self {
102 base: DirSource::Filesystem(path.as_ref().to_path_buf()),
103 buf_chunk_size: DEFAULT_CAPACITY,
104 symlink_policy: ServeDirSymlinkPolicy::default(),
105 precompressed_variants: None,
106 variant: ServeVariant::SingleFile { mime },
107 fallback: None,
108 call_fallback_on_method_not_allowed: false,
109 }
110 }
111}
112
113impl<F> ServeDir<F> {
114 rama_utils::macros::generate_set_and_with! {
115 pub fn directory_serve_mode(mut self, mode: DirectoryServeMode) -> Self {
117 match &mut self.variant {
118 ServeVariant::Directory { serve_mode, .. } => {
119 *serve_mode = mode;
120 self
121 }
122 ServeVariant::SingleFile { mime: _ } => self,
123 }
124 }
125 }
126
127 rama_utils::macros::generate_set_and_with! {
128 pub fn html_as_default_extension(mut self, html_as_default_extension: bool) -> Self {
136 match &mut self.variant {
137 ServeVariant::Directory { html_as_default_extension: dst, .. } => {
138 *dst = html_as_default_extension;
139 self
140 }
141 ServeVariant::SingleFile { mime: _ } => self,
142 }
143 }
144 }
145
146 rama_utils::macros::generate_set_and_with! {
147 pub fn buf_chunk_size(mut self, chunk_size: usize) -> Self {
151 self.buf_chunk_size = chunk_size;
152 self
153 }
154 }
155
156 rama_utils::macros::generate_set_and_with! {
157 pub fn symlink_policy(mut self, policy: ServeDirSymlinkPolicy) -> Self {
163 self.symlink_policy = policy;
164 self
165 }
166 }
167
168 rama_utils::macros::generate_set_and_with! {
169 pub fn precompressed_gzip(mut self) -> Self {
180 self.precompressed_variants
181 .get_or_insert(Default::default())
182 .gzip = true;
183 self
184 }
185 }
186
187 rama_utils::macros::generate_set_and_with! {
188 pub fn precompressed_br(mut self) -> Self {
199 self.precompressed_variants
200 .get_or_insert_default()
201 .br = true;
202 self
203 }
204 }
205
206 rama_utils::macros::generate_set_and_with! {
207 pub fn precompressed_deflate(mut self) -> Self {
218 self.precompressed_variants
219 .get_or_insert_default()
220 .deflate = true;
221 self
222 }
223 }
224
225 rama_utils::macros::generate_set_and_with! {
226 pub fn precompressed_zstd(mut self) -> Self {
237 self.precompressed_variants
238 .get_or_insert_default()
239 .zstd = true;
240 self
241 }
242 }
243
244 pub fn fallback<F2>(self, new_fallback: F2) -> ServeDir<F2> {
251 ServeDir {
252 base: self.base,
253 buf_chunk_size: self.buf_chunk_size,
254 symlink_policy: self.symlink_policy,
255 precompressed_variants: self.precompressed_variants,
256 variant: self.variant,
257 fallback: Some(new_fallback),
258 call_fallback_on_method_not_allowed: self.call_fallback_on_method_not_allowed,
259 }
260 }
261
262 #[must_use]
266 pub fn not_found_service<F2>(self, new_fallback: F2) -> ServeDir<SetStatus<F2>> {
267 self.fallback(SetStatus::new(new_fallback, StatusCode::NOT_FOUND))
268 }
269
270 rama_utils::macros::generate_set_and_with! {
271 pub fn call_fallback_on_method_not_allowed(mut self, call_fallback: bool) -> Self {
275 self.call_fallback_on_method_not_allowed = call_fallback;
276 self
277 }
278 }
279
280 pub async fn try_call<ReqBody, FResBody>(
291 &self,
292 req: Request<ReqBody>,
293 ) -> Result<Response, std::io::Error>
294 where
295 F: Service<Request<ReqBody>, Output = Response<FResBody>, Error = Infallible> + Clone,
296 FResBody: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
297 {
298 if req.method() != Method::GET && req.method() != Method::HEAD {
299 if self.call_fallback_on_method_not_allowed
300 && let Some(fallback) = self.fallback.as_ref()
301 {
302 return future::serve_fallback(fallback, req).await;
303 }
304
305 return Ok(future::method_not_allowed());
306 }
307
308 let (mut parts, body) = req.into_parts();
313 let extensions = std::mem::take(&mut parts.extensions);
315 let req = Request::from_parts(parts, Body::empty());
316
317 let fallback_and_request = self.fallback.as_ref().map(|fallback| {
318 let mut fallback_req = Request::new(body);
319 *fallback_req.method_mut() = req.method().clone();
320 *fallback_req.uri_mut() = req.uri().clone();
321 *fallback_req.headers_mut() = req.headers().clone();
322 let (mut fallback_parts, fallback_body) = fallback_req.into_parts();
326 fallback_parts.extensions = extensions;
327 let fallback_req = Request::from_parts(fallback_parts, fallback_body);
328
329 (fallback, fallback_req)
330 });
331
332 let canonical_uri = req.uri().clone().canonicalize();
337
338 let requested_path = canonical_uri.path_or_root();
339 let Some(path_to_file) = self
340 .variant
341 .build_and_validate_path(&self.base, requested_path.as_ref())
342 else {
343 return if let Some((fallback, request)) = fallback_and_request {
344 future::serve_fallback(fallback, request).await
345 } else {
346 Ok(future::not_found())
347 };
348 };
349
350 let buf_chunk_size = self.buf_chunk_size;
351 let range_header = req
352 .headers()
353 .get(header::RANGE)
354 .and_then(|value| value.to_str().ok())
355 .map(|s| s.to_owned());
356
357 let precompression_configured = self.precompressed_variants.is_some();
358 let negotiated_encodings: Vec<_> = parse_accept_encoding_headers(
359 req.headers(),
360 self.precompressed_variants.unwrap_or_default(),
361 )
362 .collect();
363
364 let variant = self.variant.clone();
365 let open_file_result = open_file::open_file(
366 variant,
367 path_to_file,
368 req,
369 negotiated_encodings,
370 range_header.as_deref(),
371 buf_chunk_size,
372 &self.base,
373 precompression_configured,
374 self.symlink_policy,
375 )
376 .await;
377
378 future::consume_open_file_result(open_file_result, fallback_and_request).await
379 }
380}
381
382impl<ReqBody, F, FResBody> Service<Request<ReqBody>> for ServeDir<F>
383where
384 ReqBody: Send + 'static,
385 F: Service<Request<ReqBody>, Output = Response<FResBody>, Error = Infallible> + Clone,
386 FResBody: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
387{
388 type Output = Response;
389 type Error = Infallible;
390
391 async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
392 let result = self.try_call(req).await;
393 Ok(result.unwrap_or_else(|err| {
394 tracing::error!("Failed to read file: {err:?}");
395 StatusCode::INTERNAL_SERVER_ERROR.into_response()
396 }))
397 }
398}
399
400#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
417pub enum ServeDirSymlinkPolicy {
418 #[default]
419 RejectAll,
421 AllowFinalComponent,
424 AllowAll,
426}
427
428impl fmt::Display for ServeDirSymlinkPolicy {
429 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
430 write!(
431 f,
432 "{}",
433 match self {
434 Self::RejectAll => "reject-all",
435 Self::AllowFinalComponent => "allow-final-component",
436 Self::AllowAll => "allow-all",
437 }
438 )
439 }
440}
441
442impl FromStr for ServeDirSymlinkPolicy {
443 type Err = BoxError;
444
445 fn from_str(s: &str) -> Result<Self, Self::Err> {
446 rama_utils::macros::match_ignore_ascii_case_str! {
447 match(s) {
448 "reject-all" | "reject_all" => Ok(Self::RejectAll),
449 "allow-final-component" | "allow_final_component" => Ok(Self::AllowFinalComponent),
450 "allow-all" | "allow_all" => Ok(Self::AllowAll),
451 _ => Err(BoxError::from_static_str("invalid ServeDirSymlinkPolicy str")),
452 }
453 }
454 }
455}
456
457impl TryFrom<&str> for ServeDirSymlinkPolicy {
458 type Error = BoxError;
459
460 #[inline]
461 fn try_from(value: &str) -> Result<Self, Self::Error> {
462 value.parse()
463 }
464}
465
466#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
467pub enum DirectoryServeMode {
468 #[default]
469 AppendIndexHtml,
473 NotFound,
476 #[cfg(feature = "html")]
479 HtmlFileList,
480}
481
482impl fmt::Display for DirectoryServeMode {
483 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
484 write!(
485 f,
486 "{}",
487 match self {
488 Self::AppendIndexHtml => "append-index",
489 Self::NotFound => "not-found",
490 #[cfg(feature = "html")]
491 Self::HtmlFileList => "html-file-list",
492 }
493 )
494 }
495}
496
497impl FromStr for DirectoryServeMode {
498 type Err = BoxError;
499
500 fn from_str(s: &str) -> Result<Self, Self::Err> {
501 rama_utils::macros::match_ignore_ascii_case_str! {
502 match(s) {
503 "append-index" | "append_index" => Ok(Self::AppendIndexHtml),
504 "not-found" | "not_found" => Ok(Self::NotFound),
505 "html-file-list" | "html_file_list" => html_file_list_from_str(),
506 _ => Err(BoxError::from_static_str("invalid DirectoryServeMode str")),
507 }
508 }
509 }
510}
511
512#[inline(always)]
513fn html_file_list_from_str() -> Result<DirectoryServeMode, BoxError> {
514 #[cfg(feature = "html")]
515 {
516 Ok(DirectoryServeMode::HtmlFileList)
517 }
518 #[cfg(not(feature = "html"))]
519 {
520 Err(BoxError::from_static_str(
521 "invalid DirectoryServeMode str: html file list requires html feature",
522 ))
523 }
524}
525
526impl TryFrom<&str> for DirectoryServeMode {
527 type Error = BoxError;
528
529 #[inline]
530 fn try_from(value: &str) -> Result<Self, Self::Error> {
531 value.parse()
532 }
533}
534
535#[derive(Clone, Debug)]
538enum ServeVariant {
539 Directory {
540 serve_mode: DirectoryServeMode,
541 html_as_default_extension: bool,
544 },
545 SingleFile {
546 mime: Mime,
547 },
548}
549
550impl ServeVariant {
551 fn build_and_validate_path(&self, source: &DirSource, requested_path: &str) -> Option<PathBuf> {
554 match self {
555 Self::Directory { .. } => {
556 let path = requested_path.trim_start_matches('/');
557
558 let path_decoded = percent_decode(path.as_ref()).decode_utf8().ok()?;
559
560 let relative = rama_utils::fs::sanitize_relative_path(&*path_decoded).ok()?;
565
566 let mut path_to_file = match source {
567 DirSource::Filesystem(base_path) => base_path.clone(),
568 DirSource::Embedded(_) => PathBuf::new(), };
570 path_to_file.push(relative);
571 Some(path_to_file)
572 }
573 Self::SingleFile { mime: _ } => match source {
574 DirSource::Filesystem(base_path) => Some(base_path.clone()),
575 DirSource::Embedded(_) => Some(PathBuf::new()), },
577 }
578 }
579}
580
581#[derive(Debug, Clone, Copy)]
583pub struct DefaultServeDirFallback(Infallible);
584
585impl<ReqBody> Service<Request<ReqBody>> for DefaultServeDirFallback
586where
587 ReqBody: Send + 'static,
588{
589 type Output = Response;
590 type Error = Infallible;
591
592 async fn serve(&self, _req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
593 match self.0 {}
594 }
595}
596
597#[derive(Clone, Copy, Debug, Default)]
598struct PrecompressedVariants {
599 gzip: bool,
600 deflate: bool,
601 br: bool,
602 zstd: bool,
603}
604
605impl SupportedEncodings for PrecompressedVariants {
606 fn gzip(&self) -> bool {
607 self.gzip
608 }
609
610 fn deflate(&self) -> bool {
611 self.deflate
612 }
613
614 fn br(&self) -> bool {
615 self.br
616 }
617
618 fn zstd(&self) -> bool {
619 self.zstd
620 }
621}