tty_web/web/
static_files.rs

1//! Static file serving with compile-time embedded frontend assets.
2//!
3//! All files under `frontend/` are embedded into the binary via
4//! [`rust_embed`]. MIME types are detected automatically.
5
6use axum::body::Body;
7use axum::extract::Path;
8use axum::http::{HeaderValue, StatusCode, header};
9use axum::response::{IntoResponse, Response};
10use rust_embed::RustEmbed;
11
12/// Frontend assets embedded at compile time.
13#[derive(RustEmbed)]
14#[folder = "frontend/"]
15struct Assets;
16
17pub async fn index() -> impl IntoResponse {
18    serve_file("index.html")
19}
20
21pub async fn static_file(Path(path): Path<String>) -> impl IntoResponse {
22    serve_file(&path)
23}
24
25fn serve_file(path: &str) -> Response {
26    let Some(file) = Assets::get(path) else {
27        return StatusCode::NOT_FOUND.into_response();
28    };
29
30    let mime = mime_guess::from_path(path)
31        .first_or_octet_stream()
32        .to_string();
33
34    let content_type = HeaderValue::from_str(&mime)
35        .unwrap_or(HeaderValue::from_static("application/octet-stream"));
36
37    let mut response = Response::new(Body::from(file.data.to_vec()));
38    response
39        .headers_mut()
40        .insert(header::CONTENT_TYPE, content_type);
41    response.headers_mut().insert(
42        header::CACHE_CONTROL,
43        HeaderValue::from_static("public, max-age=3600"),
44    );
45    response
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn test_index_html() {
54        let response = serve_file("index.html");
55        assert_eq!(response.status(), StatusCode::OK);
56        let ct = response
57            .headers()
58            .get(header::CONTENT_TYPE)
59            .expect("content-type header");
60        assert!(
61            ct.to_str().unwrap().contains("text/html"),
62            "expected text/html, got: {:?}",
63            ct
64        );
65    }
66
67    #[test]
68    fn test_not_found() {
69        let response = serve_file("nonexistent_file.xyz");
70        assert_eq!(response.status(), StatusCode::NOT_FOUND);
71    }
72}