rama_http/service/fs/serve_dir/
mod.rs

1use crate::dep::http_body::{self, Body as HttpBody};
2use crate::layer::set_status::SetStatus;
3use crate::{Body, HeaderValue, Method, Request, Response, StatusCode, header};
4use bytes::Bytes;
5use percent_encoding::percent_decode;
6use rama_core::error::{BoxError, OpaqueError};
7use rama_core::{Context, Service};
8use rama_http_types::headers::encoding::{SupportedEncodings, parse_accept_encoding_headers};
9use std::fmt;
10use std::str::FromStr;
11use std::{
12    convert::Infallible,
13    path::{Component, Path, PathBuf},
14};
15
16pub(crate) mod future;
17mod headers;
18mod open_file;
19
20#[cfg(test)]
21mod tests;
22
23// default capacity 64KiB
24const DEFAULT_CAPACITY: usize = 65536;
25
26/// Service that serves files from a given directory and all its sub directories.
27///
28/// The `Content-Type` will be guessed from the file extension.
29///
30/// An empty response with status `404 Not Found` will be returned if:
31///
32/// - The file doesn't exist
33/// - Any segment of the path contains `..`
34/// - Any segment of the path contains a backslash
35/// - On unix, any segment of the path referenced as directory is actually an
36///   existing file (`/file.html/something`)
37/// - We don't have necessary permissions to read the file
38///
39/// # Example
40///
41/// ```rust,no_run
42/// use rama_http_backend::server::HttpServer;
43/// use rama_http::service::fs::{ServeDir, ServeFile};
44/// use rama_core::{
45///     rt::Executor,
46///     Layer, layer::TraceErrLayer,
47/// };
48/// use rama_tcp::server::TcpListener;
49///
50/// #[tokio::main]
51/// async fn main() {
52///     let exec = Executor::default();
53///
54///     let listener = TcpListener::bind("127.0.0.1:8080")
55///         .await
56///         .expect("bind TCP Listener");
57///
58///     // This will serve files in the "assets" directory and
59///     // its subdirectories
60///     let http_fs_server = HttpServer::auto(exec).service(ServeDir::new("assets"));
61///
62///     // Serve the HTTP server over TCP
63///     listener
64///         .serve(TraceErrLayer::new().into_layer(http_fs_server))
65///         .await;
66/// }
67/// ```
68#[derive(Clone, Debug)]
69pub struct ServeDir<F = DefaultServeDirFallback> {
70    base: PathBuf,
71    buf_chunk_size: usize,
72    precompressed_variants: Option<PrecompressedVariants>,
73    // This is used to specialise implementation for
74    // single files
75    variant: ServeVariant,
76    fallback: Option<F>,
77    call_fallback_on_method_not_allowed: bool,
78}
79
80impl ServeDir<DefaultServeDirFallback> {
81    /// Create a new [`ServeDir`].
82    pub fn new<P>(path: P) -> Self
83    where
84        P: AsRef<Path>,
85    {
86        let mut base = PathBuf::from(".");
87        base.push(path.as_ref());
88
89        Self {
90            base,
91            buf_chunk_size: DEFAULT_CAPACITY,
92            precompressed_variants: None,
93            variant: ServeVariant::Directory {
94                serve_mode: Default::default(),
95            },
96            fallback: None,
97            call_fallback_on_method_not_allowed: false,
98        }
99    }
100
101    pub(crate) fn new_single_file<P>(path: P, mime: HeaderValue) -> Self
102    where
103        P: AsRef<Path>,
104    {
105        Self {
106            base: path.as_ref().to_owned(),
107            buf_chunk_size: DEFAULT_CAPACITY,
108            precompressed_variants: None,
109            variant: ServeVariant::SingleFile { mime },
110            fallback: None,
111            call_fallback_on_method_not_allowed: false,
112        }
113    }
114}
115
116impl<F> ServeDir<F> {
117    /// Set the [`DirectoryServeMode`].
118    pub fn with_directory_serve_mode(mut self, mode: DirectoryServeMode) -> Self {
119        match &mut self.variant {
120            ServeVariant::Directory { serve_mode } => {
121                *serve_mode = mode;
122                self
123            }
124            ServeVariant::SingleFile { mime: _ } => self,
125        }
126    }
127
128    /// Set the [`DirectoryServeMode`].
129    pub fn set_directory_serve_mode(&mut self, mode: DirectoryServeMode) -> &mut Self {
130        match &mut self.variant {
131            ServeVariant::Directory { serve_mode } => {
132                *serve_mode = mode;
133                self
134            }
135            ServeVariant::SingleFile { mime: _ } => self,
136        }
137    }
138
139    /// Set a specific read buffer chunk size.
140    ///
141    /// The default capacity is 64kb.
142    pub fn with_buf_chunk_size(mut self, chunk_size: usize) -> Self {
143        self.buf_chunk_size = chunk_size;
144        self
145    }
146
147    /// Set a specific read buffer chunk size.
148    ///
149    /// The default capacity is 64kb.
150    pub fn set_buf_chunk_size(&mut self, chunk_size: usize) -> &mut Self {
151        self.buf_chunk_size = chunk_size;
152        self
153    }
154
155    /// Informs the service that it should also look for a precompressed gzip
156    /// version of _any_ file in the directory.
157    ///
158    /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
159    /// a client with an `Accept-Encoding` header that allows the gzip encoding
160    /// will receive the file `dir/foo.txt.gz` instead of `dir/foo.txt`.
161    /// If the precompressed file is not available, or the client doesn't support it,
162    /// the uncompressed version will be served instead.
163    /// Both the precompressed version and the uncompressed version are expected
164    /// to be present in the directory. Different precompressed variants can be combined.
165    pub fn precompressed_gzip(mut self) -> Self {
166        self.precompressed_variants
167            .get_or_insert(Default::default())
168            .gzip = true;
169        self
170    }
171
172    /// Informs the service that it should also look for a precompressed gzip
173    /// version of _any_ file in the directory.
174    ///
175    /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
176    /// a client with an `Accept-Encoding` header that allows the gzip encoding
177    /// will receive the file `dir/foo.txt.gz` instead of `dir/foo.txt`.
178    /// If the precompressed file is not available, or the client doesn't support it,
179    /// the uncompressed version will be served instead.
180    /// Both the precompressed version and the uncompressed version are expected
181    /// to be present in the directory. Different precompressed variants can be combined.
182    pub fn set_precompressed_gzip(&mut self) -> &mut Self {
183        self.precompressed_variants
184            .get_or_insert(Default::default())
185            .gzip = true;
186        self
187    }
188
189    /// Informs the service that it should also look for a precompressed brotli
190    /// version of _any_ file in the directory.
191    ///
192    /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
193    /// a client with an `Accept-Encoding` header that allows the brotli encoding
194    /// will receive the file `dir/foo.txt.br` instead of `dir/foo.txt`.
195    /// If the precompressed file is not available, or the client doesn't support it,
196    /// the uncompressed version will be served instead.
197    /// Both the precompressed version and the uncompressed version are expected
198    /// to be present in the directory. Different precompressed variants can be combined.
199    pub fn precompressed_br(mut self) -> Self {
200        self.precompressed_variants
201            .get_or_insert(Default::default())
202            .br = true;
203        self
204    }
205
206    /// Informs the service that it should also look for a precompressed brotli
207    /// version of _any_ file in the directory.
208    ///
209    /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
210    /// a client with an `Accept-Encoding` header that allows the brotli encoding
211    /// will receive the file `dir/foo.txt.br` instead of `dir/foo.txt`.
212    /// If the precompressed file is not available, or the client doesn't support it,
213    /// the uncompressed version will be served instead.
214    /// Both the precompressed version and the uncompressed version are expected
215    /// to be present in the directory. Different precompressed variants can be combined.
216    pub fn set_precompressed_br(&mut self) -> &mut Self {
217        self.precompressed_variants
218            .get_or_insert(Default::default())
219            .br = true;
220        self
221    }
222
223    /// Informs the service that it should also look for a precompressed deflate
224    /// version of _any_ file in the directory.
225    ///
226    /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
227    /// a client with an `Accept-Encoding` header that allows the deflate encoding
228    /// will receive the file `dir/foo.txt.zz` instead of `dir/foo.txt`.
229    /// If the precompressed file is not available, or the client doesn't support it,
230    /// the uncompressed version will be served instead.
231    /// Both the precompressed version and the uncompressed version are expected
232    /// to be present in the directory. Different precompressed variants can be combined.
233    pub fn precompressed_deflate(mut self) -> Self {
234        self.precompressed_variants
235            .get_or_insert(Default::default())
236            .deflate = true;
237        self
238    }
239
240    /// Informs the service that it should also look for a precompressed deflate
241    /// version of _any_ file in the directory.
242    ///
243    /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
244    /// a client with an `Accept-Encoding` header that allows the deflate encoding
245    /// will receive the file `dir/foo.txt.zz` instead of `dir/foo.txt`.
246    /// If the precompressed file is not available, or the client doesn't support it,
247    /// the uncompressed version will be served instead.
248    /// Both the precompressed version and the uncompressed version are expected
249    /// to be present in the directory. Different precompressed variants can be combined.
250    pub fn set_precompressed_deflate(&mut self) -> &mut Self {
251        self.precompressed_variants
252            .get_or_insert(Default::default())
253            .deflate = true;
254        self
255    }
256
257    /// Informs the service that it should also look for a precompressed zstd
258    /// version of _any_ file in the directory.
259    ///
260    /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
261    /// a client with an `Accept-Encoding` header that allows the zstd encoding
262    /// will receive the file `dir/foo.txt.zst` instead of `dir/foo.txt`.
263    /// If the precompressed file is not available, or the client doesn't support it,
264    /// the uncompressed version will be served instead.
265    /// Both the precompressed version and the uncompressed version are expected
266    /// to be present in the directory. Different precompressed variants can be combined.
267    pub fn precompressed_zstd(mut self) -> Self {
268        self.precompressed_variants
269            .get_or_insert(Default::default())
270            .zstd = true;
271        self
272    }
273
274    /// Informs the service that it should also look for a precompressed zstd
275    /// version of _any_ file in the directory.
276    ///
277    /// Assuming the `dir` directory is being served and `dir/foo.txt` is requested,
278    /// a client with an `Accept-Encoding` header that allows the zstd encoding
279    /// will receive the file `dir/foo.txt.zst` instead of `dir/foo.txt`.
280    /// If the precompressed file is not available, or the client doesn't support it,
281    /// the uncompressed version will be served instead.
282    /// Both the precompressed version and the uncompressed version are expected
283    /// to be present in the directory. Different precompressed variants can be combined.
284    pub fn set_precompressed_zstd(&mut self) -> &mut Self {
285        self.precompressed_variants
286            .get_or_insert(Default::default())
287            .zstd = true;
288        self
289    }
290
291    /// Set the fallback service.
292    ///
293    /// This service will be called if there is no file at the path of the request.
294    ///
295    /// The status code returned by the fallback will not be altered. Use
296    /// [`ServeDir::not_found_service`] to set a fallback and always respond with `404 Not Found`.
297    ///
298    /// # Example
299    ///
300    /// This can be used to respond with a different file:
301    ///
302    /// ```rust,no_run
303    /// use rama_core::{
304    ///     rt::Executor,
305    ///     Layer, layer::TraceErrLayer,
306    /// };
307    /// use rama_tcp::server::TcpListener;
308    /// use rama_http_backend::server::HttpServer;
309    /// use rama_http::service::fs::{ServeDir, ServeFile};
310    ///
311    /// #[tokio::main]
312    /// async fn main() {
313    ///     let exec = Executor::default();
314    ///
315    ///     let listener = TcpListener::bind("127.0.0.1:8080")
316    ///         .await
317    ///         .expect("bind TCP Listener");
318    ///
319    ///     // This will serve files in the "assets" directory and
320    ///     // its subdirectories, and use assets/not_found.html as the fallback page
321    ///     let fs_server = ServeDir::new("assets").fallback(ServeFile::new("assets/not_found.html"));
322    ///     let http_fs_server = HttpServer::auto(exec).service(fs_server);
323    ///
324    ///     // Serve the HTTP server over TCP
325    ///     listener
326    ///         .serve(TraceErrLayer::new().into_layer(http_fs_server))
327    ///         .await;
328    /// }
329    /// ```
330    pub fn fallback<F2>(self, new_fallback: F2) -> ServeDir<F2> {
331        ServeDir {
332            base: self.base,
333            buf_chunk_size: self.buf_chunk_size,
334            precompressed_variants: self.precompressed_variants,
335            variant: self.variant,
336            fallback: Some(new_fallback),
337            call_fallback_on_method_not_allowed: self.call_fallback_on_method_not_allowed,
338        }
339    }
340
341    /// Set the fallback service and override the fallback's status code to `404 Not Found`.
342    ///
343    /// This service will be called if there is no file at the path of the request.
344    ///
345    /// # Example
346    ///
347    /// This can be used to respond with a different file:
348    ///
349    /// ```rust,no_run
350    /// use rama_core::{
351    ///     rt::Executor,
352    ///     layer::TraceErrLayer,
353    ///     Layer,
354    /// };
355    /// use rama_tcp::server::TcpListener;
356    /// use rama_http_backend::server::HttpServer;
357    /// use rama_http::service::fs::{ServeDir, ServeFile};
358    ///
359    /// #[tokio::main]
360    /// async fn main() {
361    ///     let exec = Executor::default();
362    ///
363    ///     let listener = TcpListener::bind("127.0.0.1:8080")
364    ///         .await
365    ///         .expect("bind TCP Listener");
366    ///
367    ///     // This will serve files in the "assets" directory and
368    ///     // its subdirectories, and use assets/not_found.html as the not_found page
369    ///     let fs_server = ServeDir::new("assets").not_found_service(ServeFile::new("assets/not_found.html"));
370    ///     let http_fs_server = HttpServer::auto(exec).service(fs_server);
371    ///
372    ///     // Serve the HTTP server over TCP
373    ///     listener
374    ///         .serve(TraceErrLayer::new().into_layer(http_fs_server))
375    ///         .await;
376    /// }
377    /// ```
378    ///
379    /// Setups like this are often found in single page applications.
380    pub fn not_found_service<F2>(self, new_fallback: F2) -> ServeDir<SetStatus<F2>> {
381        self.fallback(SetStatus::new(new_fallback, StatusCode::NOT_FOUND))
382    }
383
384    /// Customize whether or not to call the fallback for requests that aren't `GET` or `HEAD`.
385    ///
386    /// Defaults to not calling the fallback and instead returning `405 Method Not Allowed`.
387    pub fn call_fallback_on_method_not_allowed(mut self, call_fallback: bool) -> Self {
388        self.call_fallback_on_method_not_allowed = call_fallback;
389        self
390    }
391
392    /// Customize whether or not to call the fallback for requests that aren't `GET` or `HEAD`.
393    ///
394    /// Defaults to not calling the fallback and instead returning `405 Method Not Allowed`.
395    pub fn set_call_fallback_on_method_not_allowed(&mut self, call_fallback: bool) -> &mut Self {
396        self.call_fallback_on_method_not_allowed = call_fallback;
397        self
398    }
399
400    /// Call the service and get a future that contains any `std::io::Error` that might have
401    /// happened.
402    ///
403    /// By default `<ServeDir as Service<_>>::call` will handle IO errors and convert them into
404    /// responses. It does that by converting [`std::io::ErrorKind::NotFound`] and
405    /// [`std::io::ErrorKind::PermissionDenied`] to `404 Not Found` and any other error to `500
406    /// Internal Server Error`. The error will also be logged with `tracing`.
407    ///
408    /// If you want to manually control how the error response is generated you can make a new
409    /// service that wraps a `ServeDir` and calls `try_call` instead of `call`.
410    ///
411    /// # Example
412    ///
413    /// ```rust,no_run
414    /// use rama_core::{
415    ///     rt::Executor,
416    ///     service::service_fn,
417    ///     layer::TraceErrLayer,
418    ///     Context, Layer,
419    /// };
420    /// use rama_tcp::server::TcpListener;
421    /// use rama_http_backend::server::HttpServer;
422    /// use rama_http::service::fs::ServeDir;
423    /// use rama_http::{Body, Request, Response, StatusCode};
424    /// use std::convert::Infallible;
425    ///
426    /// #[tokio::main]
427    /// async fn main() {
428    ///     let exec = Executor::default();
429    ///
430    ///     let listener = TcpListener::bind("127.0.0.1:8080")
431    ///         .await
432    ///         .expect("bind TCP Listener");
433    ///
434    ///     // This will serve files in the "assets" directory and
435    ///     // its subdirectories, and use assets/not_found.html as the fallback page
436    ///     let http_fs_server = HttpServer::auto(exec).service(service_fn(serve_dir));
437    ///
438    ///     // Serve the HTTP server over TCP
439    ///     listener
440    ///         .serve(TraceErrLayer::new().into_layer(http_fs_server))
441    ///         .await;
442    /// }
443    ///
444    /// async fn serve_dir<State>(
445    ///     ctx: Context<State>,
446    ///     request: Request,
447    /// ) -> Result<Response<Body>, Infallible>
448    /// where
449    ///     State: Clone + Send + Sync + 'static,
450    /// {
451    ///     let service = ServeDir::new("assets");
452    ///
453    ///     match service.try_call(ctx, request).await {
454    ///         Ok(response) => Ok(response),
455    ///         Err(_) => {
456    ///             let body = Body::from("Something went wrong...");
457    ///             let response = Response::builder()
458    ///                 .status(StatusCode::INTERNAL_SERVER_ERROR)
459    ///                 .body(body)
460    ///                 .unwrap();
461    ///             Ok(response)
462    ///         }
463    ///     }
464    /// }
465    /// ```
466    pub async fn try_call<State, ReqBody, FResBody>(
467        &self,
468        ctx: Context<State>,
469        req: Request<ReqBody>,
470    ) -> Result<Response, std::io::Error>
471    where
472        State: Clone + Send + Sync + 'static,
473        F: Service<State, Request<ReqBody>, Response = Response<FResBody>, Error = Infallible>
474            + Clone,
475        FResBody: http_body::Body<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
476    {
477        if req.method() != Method::GET && req.method() != Method::HEAD {
478            if self.call_fallback_on_method_not_allowed {
479                if let Some(fallback) = self.fallback.as_ref() {
480                    return future::serve_fallback(fallback, ctx, req).await;
481                }
482            } else {
483                return Ok(future::method_not_allowed());
484            }
485        }
486
487        // `ServeDir` doesn't care about the request body but the fallback might. So move out the
488        // body and pass it to the fallback, leaving an empty body in its place
489        //
490        // this is necessary because we cannot clone bodies
491        let (mut parts, body) = req.into_parts();
492        // same goes for extensions
493        let extensions = std::mem::take(&mut parts.extensions);
494        let req = Request::from_parts(parts, Body::empty());
495
496        let fallback_and_request = self.fallback.as_ref().map(|fallback| {
497            let mut fallback_req = Request::new(body);
498            *fallback_req.method_mut() = req.method().clone();
499            *fallback_req.uri_mut() = req.uri().clone();
500            *fallback_req.headers_mut() = req.headers().clone();
501            *fallback_req.extensions_mut() = extensions;
502
503            (fallback, ctx, fallback_req)
504        });
505
506        let path_to_file = match self
507            .variant
508            .build_and_validate_path(&self.base, req.uri().path())
509        {
510            Some(path_to_file) => path_to_file,
511            None => {
512                return if let Some((fallback, ctx, request)) = fallback_and_request {
513                    future::serve_fallback(fallback, ctx, request).await
514                } else {
515                    Ok(future::not_found())
516                };
517            }
518        };
519
520        let buf_chunk_size = self.buf_chunk_size;
521        let range_header = req
522            .headers()
523            .get(header::RANGE)
524            .and_then(|value| value.to_str().ok())
525            .map(|s| s.to_owned());
526
527        let negotiated_encodings: Vec<_> = parse_accept_encoding_headers(
528            req.headers(),
529            self.precompressed_variants.unwrap_or_default(),
530        )
531        .collect();
532
533        let variant = self.variant.clone();
534
535        let open_file_result = open_file::open_file(
536            variant,
537            path_to_file,
538            req,
539            negotiated_encodings,
540            range_header,
541            buf_chunk_size,
542        )
543        .await;
544
545        future::consume_open_file_result(open_file_result, fallback_and_request).await
546    }
547}
548
549impl<State, ReqBody, F, FResBody> Service<State, Request<ReqBody>> for ServeDir<F>
550where
551    State: Clone + Send + Sync + 'static,
552    ReqBody: Send + 'static,
553    F: Service<State, Request<ReqBody>, Response = Response<FResBody>, Error = Infallible> + Clone,
554    FResBody: HttpBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
555{
556    type Response = Response;
557    type Error = Infallible;
558
559    async fn serve(
560        &self,
561        ctx: Context<State>,
562        req: Request<ReqBody>,
563    ) -> Result<Self::Response, Self::Error> {
564        let result = self.try_call(ctx, req).await;
565        Ok(result.unwrap_or_else(|err| {
566            tracing::error!(error = %err, "Failed to read file");
567
568            let body = Body::empty();
569            Response::builder()
570                .status(StatusCode::INTERNAL_SERVER_ERROR)
571                .body(body)
572                .unwrap()
573        }))
574    }
575}
576
577#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
578pub enum DirectoryServeMode {
579    #[default]
580    /// If the requested path is a directory append `index.html`.
581    ///
582    /// This is useful for static sites
583    AppendIndexHtml,
584    /// If the requested path is a directory
585    /// handle it as resource "not found" (404).
586    NotFound,
587    /// Show the file tree of the directory as file tree
588    /// which can be navigated.
589    HtmlFileList,
590}
591
592impl fmt::Display for DirectoryServeMode {
593    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
594        write!(
595            f,
596            "{}",
597            match self {
598                DirectoryServeMode::AppendIndexHtml => "append-index",
599                DirectoryServeMode::NotFound => "not-found",
600                DirectoryServeMode::HtmlFileList => "html-file-list",
601            }
602        )
603    }
604}
605
606impl FromStr for DirectoryServeMode {
607    type Err = OpaqueError;
608
609    fn from_str(s: &str) -> Result<Self, Self::Err> {
610        rama_utils::macros::match_ignore_ascii_case_str! {
611            match(s) {
612                "append-index" | "append_index" => Ok(Self::AppendIndexHtml),
613                "not-found" | "not_found" => Ok(Self::NotFound),
614                "html-file-list" | "html_file_list" => Ok(Self::HtmlFileList),
615                _ => Err(OpaqueError::from_display("invalid DirectoryServeMode str")),
616            }
617        }
618    }
619}
620
621impl TryFrom<&str> for DirectoryServeMode {
622    type Error = OpaqueError;
623
624    #[inline]
625    fn try_from(value: &str) -> Result<Self, Self::Error> {
626        value.parse()
627    }
628}
629
630// Allow the ServeDir service to be used in the ServeFile service
631// with almost no overhead
632#[derive(Clone, Debug)]
633enum ServeVariant {
634    Directory { serve_mode: DirectoryServeMode },
635    SingleFile { mime: HeaderValue },
636}
637
638impl ServeVariant {
639    fn build_and_validate_path(&self, base_path: &Path, requested_path: &str) -> Option<PathBuf> {
640        match self {
641            ServeVariant::Directory { serve_mode: _ } => {
642                let path = requested_path.trim_start_matches('/');
643
644                let path_decoded = percent_decode(path.as_ref()).decode_utf8().ok()?;
645                let path_decoded = Path::new(&*path_decoded);
646
647                let mut path_to_file = base_path.to_path_buf();
648                for component in path_decoded.components() {
649                    match component {
650                        Component::Normal(comp) => {
651                            // protect against paths like `/foo/c:/bar/baz` (#204)
652                            if Path::new(&comp)
653                                .components()
654                                .all(|c| matches!(c, Component::Normal(_)))
655                            {
656                                path_to_file.push(comp)
657                            } else {
658                                return None;
659                            }
660                        }
661                        Component::CurDir => {}
662                        Component::Prefix(_) | Component::RootDir | Component::ParentDir => {
663                            return None;
664                        }
665                    }
666                }
667                Some(path_to_file)
668            }
669            ServeVariant::SingleFile { mime: _ } => Some(base_path.to_path_buf()),
670        }
671    }
672}
673
674/// The default fallback service used with [`ServeDir`].
675#[derive(Debug, Clone, Copy)]
676pub struct DefaultServeDirFallback(Infallible);
677
678impl<State, ReqBody> Service<State, Request<ReqBody>> for DefaultServeDirFallback
679where
680    State: Clone + Send + Sync + 'static,
681    ReqBody: Send + 'static,
682{
683    type Response = Response;
684    type Error = Infallible;
685
686    async fn serve(
687        &self,
688        _ctx: Context<State>,
689        _req: Request<ReqBody>,
690    ) -> Result<Self::Response, Self::Error> {
691        match self.0 {}
692    }
693}
694
695#[derive(Clone, Copy, Debug, Default)]
696struct PrecompressedVariants {
697    gzip: bool,
698    deflate: bool,
699    br: bool,
700    zstd: bool,
701}
702
703impl SupportedEncodings for PrecompressedVariants {
704    fn gzip(&self) -> bool {
705        self.gzip
706    }
707
708    fn deflate(&self) -> bool {
709        self.deflate
710    }
711
712    fn br(&self) -> bool {
713        self.br
714    }
715
716    fn zstd(&self) -> bool {
717        self.zstd
718    }
719}