volo_http/server/utils/
serve_dir.rs1use std::{
23 fs,
24 marker::PhantomData,
25 path::{Path, PathBuf},
26};
27
28use http::{header::HeaderValue, status::StatusCode};
29use motore::service::Service;
30
31use super::FileResponse;
32use crate::{context::ServerContext, request::Request, response::Response, server::IntoResponse};
33
34pub struct ServeDir<E, F> {
36 path: PathBuf,
37 mime_getter: F,
38 _marker: PhantomData<fn(E)>,
39}
40
41impl<E> ServeDir<E, fn(&Path) -> HeaderValue> {
42 pub fn new<P>(path: P) -> Self
49 where
50 P: AsRef<Path>,
51 {
52 let path = fs::canonicalize(path).expect("ServeDir: failed to canonicalize path");
53 assert!(path.is_dir());
54 Self {
55 path,
56 mime_getter: guess_mime,
57 _marker: PhantomData,
58 }
59 }
60
61 pub fn mime_getter<F>(self, mime_getter: F) -> ServeDir<E, F>
66 where
67 F: Fn(&Path) -> HeaderValue,
68 {
69 ServeDir {
70 path: self.path,
71 mime_getter,
72 _marker: self._marker,
73 }
74 }
75}
76
77impl<B, E, F> Service<ServerContext, Request<B>> for ServeDir<E, F>
78where
79 B: Send,
80 F: Fn(&Path) -> HeaderValue + Sync,
81{
82 type Response = Response;
83 type Error = E;
84
85 async fn call(
86 &self,
87 _: &mut ServerContext,
88 req: Request<B>,
89 ) -> Result<Self::Response, Self::Error> {
90 let path = req.uri().path();
92 let path = path.strip_prefix('/').unwrap_or(path);
93
94 tracing::trace!("[Volo-HTTP] ServeDir: path: {path}");
95
96 let path = self.path.join(path);
98 let Ok(path) = fs::canonicalize(path) else {
99 return Ok(StatusCode::NOT_FOUND.into_response());
100 };
101
102 if path.strip_prefix(self.path.as_path()).is_err() {
104 tracing::debug!("[Volo-HTTP] ServeDir: illegal path: {}", path.display());
105 return Ok(StatusCode::FORBIDDEN.into_response());
106 }
107
108 if !path.is_file() {
110 return Ok(StatusCode::NOT_FOUND.into_response());
111 }
112
113 let content_type = (self.mime_getter)(&path);
115 let Ok(resp) = FileResponse::new(path, content_type) else {
116 return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response());
117 };
118 Ok(resp.into_response())
119 }
120}
121
122pub fn guess_mime(path: &Path) -> HeaderValue {
123 mime_guess::from_path(path)
124 .first_raw()
125 .map(HeaderValue::from_static)
126 .unwrap_or_else(|| HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap())
127}
128
129#[cfg(test)]
130mod serve_dir_tests {
131 use http::{StatusCode, method::Method};
132
133 use super::ServeDir;
134 use crate::{
135 body::Body,
136 server::{Router, Server},
137 };
138
139 #[tokio::test]
140 async fn read_file() {
141 let router: Router<Option<Body>> =
143 Router::new().nest_service("/static/", ServeDir::new("."));
144 let server = Server::new(router).into_test_server();
145 assert!(
147 server
148 .call_route(Method::GET, "/static/Cargo.toml", None)
149 .await
150 .status()
151 .is_success()
152 );
153 assert!(
155 server
156 .call_route(Method::GET, "/static/src/lib.rs", None)
157 .await
158 .status()
159 .is_success()
160 );
161 assert_eq!(
163 server
164 .call_route(Method::GET, "/static/Cargo.lock", None)
165 .await
166 .status(),
167 StatusCode::NOT_FOUND
168 );
169 assert_eq!(
171 server
172 .call_route(Method::GET, "/static/../Cargo.toml", None)
173 .await
174 .status(),
175 StatusCode::FORBIDDEN
176 );
177 }
178}