salvo_serve_static/
lib.rs1#![doc(html_favicon_url = "https://salvo.rs/favicon-32x32.png")]
10#![doc(html_logo_url = "https://salvo.rs/images/logo.svg")]
11#![cfg_attr(docsrs, feature(doc_cfg))]
12
13pub mod dir;
14mod file;
15
16pub use dir::StaticDir;
17pub use file::StaticFile;
18use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
19use salvo_core::Response;
20use salvo_core::http::uri::{Parts as UriParts, Uri};
21use salvo_core::writing::Redirect;
22
23const HTML_ENCODE_SET: &AsciiSet = &CONTROLS
24 .add(b'\'')
25 .add(b'"')
26 .add(b'`')
27 .add(b'<')
28 .add(b'>')
29 .add(b'&');
30
31#[macro_use]
32mod cfg;
33
34#[doc(hidden)]
35#[macro_export]
36macro_rules! join_path {
37 ($($part:expr),+) => {
38 {
39 let mut p = std::path::PathBuf::new();
40 $(
41 p.push($part);
42 )*
43 path_slash::PathBufExt::to_slash_lossy(&p).to_string()
44 }
45 }
46}
47
48cfg_feature! {
49 #![feature = "embed"]
50 mod embed;
51 pub use embed::{render_embedded_file, static_embed, EmbeddedFileExt, StaticEmbed};
52}
53
54#[inline]
55pub(crate) fn encode_url_path(path: &str) -> String {
56 path.split('/')
57 .map(|s| utf8_percent_encode(s, HTML_ENCODE_SET).to_string())
58 .collect::<Vec<_>>()
59 .join("/")
60}
61
62#[inline]
63pub(crate) fn decode_url_path_safely(path: &str) -> String {
64 percent_encoding::percent_decode_str(path)
65 .decode_utf8_lossy()
66 .to_string()
67}
68
69#[inline]
70pub(crate) fn format_url_path_safely(path: &str) -> String {
71 let final_slash = if path.ends_with('/') { "/" } else { "" };
72 let mut used_parts = Vec::with_capacity(8);
73 for part in path.split(['/', '\\']) {
74 if part.is_empty() || part == "." || (cfg!(windows) && part.contains(':')) {
75 continue;
76 }
77 if part == ".." {
78 used_parts.pop();
79 } else {
80 used_parts.push(part);
81 }
82 }
83 used_parts.join("/") + final_slash
84}
85
86pub(crate) fn redirect_to_dir_url(req_uri: &Uri, res: &mut Response) {
87 let UriParts {
88 scheme,
89 authority,
90 path_and_query,
91 ..
92 } = req_uri.clone().into_parts();
93 let mut builder = Uri::builder();
94 if let Some(scheme) = scheme {
95 builder = builder.scheme(scheme);
96 }
97 if let Some(authority) = authority {
98 builder = builder.authority(authority);
99 }
100 if let Some(path_and_query) = path_and_query {
101 if let Some(query) = path_and_query.query() {
102 builder = builder.path_and_query(format!("{}/?{}", path_and_query.path(), query));
103 } else {
104 builder = builder.path_and_query(format!("{}/", path_and_query.path()));
105 }
106 }
107 let redirect_uri = builder.build().expect("Invalid uri");
108 res.render(Redirect::found(redirect_uri));
109}
110
111#[cfg(test)]
112mod tests {
113 use salvo_core::prelude::*;
114 use salvo_core::test::{ResponseExt, TestClient};
115
116 use crate::*;
117
118 #[tokio::test]
119 async fn test_serve_static_dir() {
120 let router = Router::with_path("{*path}").get(
121 StaticDir::new(vec!["test/static"])
122 .include_dot_files(false)
123 .auto_list(true)
124 .defaults("index.html"),
125 );
126 let service = Service::new(router);
127
128 async fn access(service: &Service, accept: &str, url: &str) -> String {
129 TestClient::get(url)
130 .add_header("accept", accept, true)
131 .send(service)
132 .await
133 .take_string()
134 .await
135 .unwrap()
136 }
137 let content = access(&service, "text/plain", "http://127.0.0.1:5801").await;
138 assert!(content.contains("Index page"));
139 let content = access(&service, "text/plain", "http://127.0.0.1:5801/").await;
140 assert!(content.contains("Index page"));
141
142 let content = access(&service, "text/plain", "http://127.0.0.1:5801/dir1/").await;
143 assert!(content.contains("test3.txt") && content.contains("dir2"));
144
145 let content = access(&service, "text/xml", "http://127.0.0.1:5801/dir1/").await;
146 assert!(
147 content.starts_with("<list>")
148 && content.contains("test3.txt")
149 && content.contains("dir2")
150 );
151
152 let content = access(&service, "text/html", "http://127.0.0.1:5801/dir1/").await;
153 assert!(
154 content.contains("<html>") && content.contains("test3.txt") && content.contains("dir2")
155 );
156
157 let content = access(&service, "application/json", "http://127.0.0.1:5801/dir1/").await;
158 assert!(
159 content.starts_with('{') && content.contains("test3.txt") && content.contains("dir2")
160 );
161
162 let content = access(&service, "text/plain", "http://127.0.0.1:5801/test1.txt").await;
163 assert!(content.contains("copy1"));
164
165 let content = access(&service, "text/plain", "http://127.0.0.1:5801/test3.txt").await;
166 assert!(content.contains("Not Found"));
167
168 let content = access(
169 &service,
170 "text/plain",
171 "http://127.0.0.1:5801/../girl/love/eat.txt",
172 )
173 .await;
174 assert!(content.contains("Not Found"));
175 let content = access(
176 &service,
177 "text/plain",
178 "http://127.0.0.1:5801/..\\girl\\love\\eat.txt",
179 )
180 .await;
181 assert!(content.contains("Not Found"));
182
183 let content = access(
184 &service,
185 "text/plain",
186 "http://127.0.0.1:5801/dir1/test3.txt",
187 )
188 .await;
189 assert!(content.contains("copy3"));
190 let content = access(
191 &service,
192 "text/plain",
193 "http://127.0.0.1:5801/dir1/dir2/test3.txt",
194 )
195 .await;
196 assert!(content == "dir2 test3");
197 let content = access(
198 &service,
199 "text/plain",
200 "http://127.0.0.1:5801/dir1/../dir1/test3.txt",
201 )
202 .await;
203 assert!(content == "copy3");
204 let content = access(
205 &service,
206 "text/plain",
207 "http://127.0.0.1:5801/dir1\\..\\dir1\\test3.txt",
208 )
209 .await;
210 assert!(content == "copy3");
211 }
212
213 #[tokio::test]
214 async fn test_serve_static_file() {
215 let router = Router::new()
216 .push(
217 Router::with_path("test1.txt")
218 .get(StaticFile::new("test/static/test1.txt").chunk_size(1024)),
219 )
220 .push(
221 Router::with_path("notexist.txt").get(StaticFile::new("test/static/notexist.txt")),
222 );
223 let service = Service::new(router);
224
225 let mut response = TestClient::get("http://127.0.0.1:5801/test1.txt")
226 .send(&service)
227 .await;
228 assert_eq!(response.status_code.unwrap(), StatusCode::OK);
229 assert_eq!(response.take_string().await.unwrap(), "copy1");
230
231 let response = TestClient::get("http://127.0.0.1:5801/notexist.txt")
232 .send(&service)
233 .await;
234 assert_eq!(response.status_code.unwrap(), StatusCode::NOT_FOUND);
235 }
236
237 #[cfg(feature = "embed")]
238 #[tokio::test]
239 async fn test_serve_embed_files() {
240 #[derive(rust_embed::RustEmbed)]
241 #[folder = "test/static"]
242 struct Assets;
243
244 let router = Router::new()
245 .push(
246 Router::with_path("test1.txt")
247 .get(Assets::get("test1.txt").unwrap().into_handler()),
248 )
249 .push(Router::with_path("files/{**path}").get(serve_file))
250 .push(
251 Router::with_path("dir/{**path}").get(
252 static_embed::<Assets>()
253 .defaults("index.html")
254 .fallback("fallback.html"),
255 ),
256 )
257 .push(Router::with_path("dir2/{**path}").get(static_embed::<Assets>()))
258 .push(
259 Router::with_path("dir3/{**path}")
260 .get(static_embed::<Assets>().fallback("notexist.html")),
261 );
262 let service = Service::new(router);
263
264 #[handler]
265 async fn serve_file(req: &mut Request, res: &mut Response) {
266 let path = req.param::<String>("path").unwrap();
267 if let Some(file) = Assets::get(&path) {
268 file.render(req, res);
269 }
270 }
271
272 let mut response = TestClient::get("http://127.0.0.1:5801/files/test1.txt")
273 .send(&service)
274 .await;
275 assert_eq!(response.status_code.unwrap(), StatusCode::OK);
276 assert_eq!(response.take_string().await.unwrap(), "copy1");
277
278 let mut response = TestClient::get("http://127.0.0.1:5801/dir/test1.txt")
279 .send(&service)
280 .await;
281 assert_eq!(response.status_code.unwrap(), StatusCode::OK);
282 assert_eq!(response.take_string().await.unwrap(), "copy1");
283
284 let mut response = TestClient::get("http://127.0.0.1:5801/dir/test1111.txt")
285 .send(&service)
286 .await;
287 assert_eq!(response.status_code.unwrap(), StatusCode::OK);
288 assert!(
289 response
290 .take_string()
291 .await
292 .unwrap()
293 .contains("Fallback page")
294 );
295
296 let response = TestClient::get("http://127.0.0.1:5801/dir")
297 .send(&service)
298 .await;
299 assert_eq!(response.status_code.unwrap(), StatusCode::OK);
300
301 let mut response = TestClient::get("http://127.0.0.1:5801/dir/")
302 .send(&service)
303 .await;
304 assert_eq!(response.status_code.unwrap(), StatusCode::OK);
305 assert!(response.take_string().await.unwrap().contains("Index page"));
306
307 let response = TestClient::get("http://127.0.0.1:5801/dir2/")
308 .send(&service)
309 .await;
310 assert_eq!(response.status_code.unwrap(), StatusCode::NOT_FOUND);
311
312 let response = TestClient::get("http://127.0.0.1:5801/dir3/abc.txt")
313 .send(&service)
314 .await;
315 assert_eq!(response.status_code.unwrap(), StatusCode::NOT_FOUND);
316 }
317}