1use super::Endpoint;
2use crate::{Request, Response};
3use anyhow::Error;
4use std::path::{Path, PathBuf};
5use std::pin::Pin;
6use tokio_util::io::ReaderStream;
7
8#[derive(Debug, Clone)]
9pub(super) struct DirEndpoint {
10 base: PathBuf,
11}
12
13impl DirEndpoint {
14 pub(super) fn new<P: Into<PathBuf>>(path: P) -> Self {
15 DirEndpoint { base: path.into() }
16 }
17}
18
19#[async_trait]
20impl Endpoint for DirEndpoint {
21 async fn apply(self: Pin<&Self>, request: Request) -> Result<Response, Error> {
22 let uri_path = request.uri().path();
23 match resolve_path(request.fragment::<String, _>(1), &self.base) {
24 Some(path) => resolve_file(path, uri_path).await,
25 None => Ok(Response::empty_404()),
26 }
27 }
28}
29
30fn resolve_path(param: Option<String>, base: &Path) -> Option<PathBuf> {
31 let param = param?;
32
33 let split = param.split('/');
34 let is_invalid = split.clone().any(|v| v == ".." || v.contains('\\'));
35
36 if is_invalid {
37 return None;
38 }
39
40 let request = split.filter(|p| !p.is_empty() && *p != ".");
41 let mut buffer = base.to_path_buf();
42 request.for_each(|p| buffer.push(p));
43 Some(buffer)
44}
45
46async fn resolve_file(mut path: PathBuf, request: &str) -> Result<Response, Error> {
47 match tokio::fs::metadata(&path).await {
48 Ok(meta) if meta.is_dir() && !request.ends_with('/') => {
49 return Response::permanent_redirect(format!("{request}/")).map_err(Error::from);
50 }
51 Ok(meta) if meta.is_dir() => {
52 path.push("index.html");
53 if !tokio::fs::metadata(&path)
54 .await
55 .map(|m| m.is_file())
56 .unwrap_or(false)
57 {
58 return Ok(Response::empty_404());
59 }
60 }
61 Ok(_) => {}
62 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Response::empty_404()),
63 Err(e) => return Err(e.into()),
64 }
65
66 load_file(tokio::fs::File::open(&path).await?, &path)
67}
68
69fn load_file(file: tokio::fs::File, path: &Path) -> Result<Response, Error> {
70 let mime_type = mime_guess::MimeGuess::from_path(path).first_or_octet_stream();
71 hyper::Response::builder()
72 .header(http::header::CONTENT_TYPE, mime_type.to_string())
73 .status(hyper::StatusCode::OK)
74 .body(hyper::Body::wrap_stream(ReaderStream::new(file)))
75 .map(Response::from)
76 .map_err(Error::from)
77}