Skip to main content

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}