kellnr_embedded_resources/
lib.rs

1use std::borrow::Cow;
2
3use axum::body::Body;
4use axum::http::{Response, StatusCode, Uri, header};
5use bytes::Bytes;
6use include_dir::{Dir, include_dir};
7use mime_guess::from_path;
8use tracing::warn;
9
10static STATIC_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/static");
11
12fn cache_control_for_path(path: &str) -> &'static str {
13    // For SPAs, avoid caching `index.html` aggressively so deploys update quickly.
14    if path.ends_with("index.html") || path == "index.html" {
15        "no-cache"
16    } else {
17        // Most assets are fingerprinted by Vite (e.g. `index-<hash>.js`), so we can cache hard.
18        "public, max-age=31536000, immutable"
19    }
20}
21
22fn serve_embedded_asset(path: &str) -> Response<Body> {
23    let normalized = path.trim_start_matches('/');
24
25    if let Some(file) = STATIC_DIR.get_file(normalized) {
26        let mime = from_path(normalized).first_or_octet_stream();
27        let body = Body::from(Bytes::copy_from_slice(file.contents()));
28
29        Response::builder()
30            .status(StatusCode::OK)
31            .header(header::CONTENT_TYPE, mime.as_ref())
32            .header(header::CACHE_CONTROL, cache_control_for_path(normalized))
33            .body(body)
34            .unwrap()
35    } else {
36        warn!(path = normalized, "embedded ui asset not found");
37        Response::builder()
38            .status(StatusCode::NOT_FOUND)
39            .body(Body::from(Cow::Borrowed("404 Not Found")))
40            .unwrap()
41    }
42}
43
44// Handler has to be async to fit into axum routing, even though we don't do any async work here.
45#[allow(clippy::unused_async)]
46pub async fn embedded_static_handler(uri: Uri) -> Response<Body> {
47    let path = uri.path();
48
49    if path == "/" {
50        return serve_embedded_asset("index.html");
51    }
52
53    // Try serving embedded file if it exists
54    let candidate = path.trim_start_matches('/');
55    if STATIC_DIR.get_file(candidate).is_some() {
56        return serve_embedded_asset(candidate);
57    }
58
59    // SPA fallback: for any non-asset route, serve `index.html`
60    // (but keep true 404s for unknown files under /assets or /img etc.)
61    let looks_like_asset = candidate.contains('.')
62        || candidate.starts_with("assets/")
63        || candidate.starts_with("img/");
64    if looks_like_asset {
65        return Response::builder()
66            .status(StatusCode::NOT_FOUND)
67            .body(Body::from(Cow::Borrowed("404 Not Found")))
68            .unwrap();
69    }
70
71    serve_embedded_asset("index.html")
72}