siderite_core/static_files/mod.rs
1//! Static file serving, and single-page application mounts.
2
3use crate::app::App;
4use std::path::PathBuf;
5
6mod spa;
7
8impl App {
9 /// Serve files from `dir` under URL `prefix` (for example `/assets`).
10 ///
11 /// The mount is not documented in OpenAPI. Path traversal (`..` segments)
12 /// is rejected by `tower-http`'s `ServeDir`. A root prefix (`/` or empty)
13 /// serves `dir` as the fallback for every path no route matches.
14 #[must_use]
15 pub fn static_files(mut self, prefix: &str, dir: impl Into<PathBuf>) -> Self {
16 let prefix = prefix.trim_end_matches('/').to_owned();
17 let dir = dir.into();
18 if !prefix.is_empty() && !prefix.starts_with('/') {
19 self.config_errors.push(format!(
20 "static files prefix `{prefix}` must start with `/`"
21 ));
22 return self;
23 }
24 if prefix.is_empty() {
25 // Unknown paths that are not files still get the 404 problem document.
26 let not_found =
27 axum::handler::HandlerWithoutStateExt::into_service(crate::app::route_not_found);
28 let service = tower_http::services::ServeDir::new(dir).fallback(not_found);
29 self.fallback = Some(Box::new(move |router: axum::Router| {
30 router.fallback_service(service)
31 }));
32 } else {
33 let service = tower_http::services::ServeDir::new(dir);
34 self.services.push(Box::new(move |router: axum::Router| {
35 router.nest_service(&prefix, service)
36 }));
37 }
38 self
39 }
40
41 /// Serve a single-page application from `dir` under `prefix` (`/`
42 /// for the whole site): files as they are, and `dir/index.html` for
43 /// browser navigations to any other path below `prefix`, so client
44 /// side routes (`/app/settings`) load the app.
45 ///
46 /// Explicit routes still win. The fallback applies only to `GET` and
47 /// `HEAD` requests accepting `text/html` whose last path segment has
48 /// no extension; a missing asset, an API-style request or a missing
49 /// `index.html` answer with the 404 problem document, and other
50 /// methods with 405. Paths are resolved inside the canonical `dir`
51 /// (no `..`, encoded or not, and no symbolic link leading out). The
52 /// mount is not documented in OpenAPI.
53 #[must_use]
54 pub fn spa(mut self, prefix: &str, dir: impl Into<PathBuf>) -> Self {
55 let prefix = prefix.trim_end_matches('/').to_owned();
56 if !prefix.is_empty() && !prefix.starts_with('/') {
57 self.config_errors
58 .push(format!("SPA prefix `{prefix}` must start with `/`"));
59 return self;
60 }
61 let root = dir.into();
62 let serve = move |request| spa::serve(root.clone(), request);
63 let service = tower::service_fn(serve);
64 if prefix.is_empty() {
65 self.fallback = Some(Box::new(move |router: axum::Router| {
66 router.fallback_service(service)
67 }));
68 } else {
69 self.services.push(Box::new(move |router: axum::Router| {
70 router.nest_service(&prefix, service)
71 }));
72 }
73 self
74 }
75}