madhyamas-api 0.1.6

Open-source HTTP/HTTPS debugging proxy with web-based UI
//! Embedded web UI assets
//!
//! Web assets are embedded at compile time using `rust-embed` so the
//! binary is fully self-contained — no external `web/dist/` directory needed.
//! A `MADHYAMAS_WEB_DIR` env var can still override to serve from disk
//! (useful for development).
//!
//! When the `embedded-assets` feature is disabled (e.g. when publishing to
//! crates.io), only disk-based serving is available.

use axum::body::Body;
use axum::http::{header, Response, StatusCode};
use axum::response::IntoResponse;

#[cfg(feature = "embedded-assets")]
use axum::http::HeaderValue;

#[cfg(feature = "embedded-assets")]
use rust_embed::RustEmbed;

#[cfg(feature = "embedded-assets")]
#[derive(RustEmbed)]
#[folder = "../../web/dist/"]
struct WebAssets;

/// Serve an embedded file by path (e.g. "index.html", "assets/index-abc.js").
///
/// Returns `Some(Response)` if the file exists, `None` otherwise.
/// Falls back to `index.html` for unknown paths (SPA routing).
#[cfg(feature = "embedded-assets")]
pub fn serve_embedded(path: &str) -> Option<Response<Body>> {
    // Normalize: strip leading slash, default to index.html
    let path = path.trim_start_matches('/');

    // Try the exact path first, then SPA fallback to index.html
    let (file, served_path) = match WebAssets::get(path) {
        Some(f) => (f, path),
        None => {
            // SPA fallback: serve index.html for non-asset routes
            if !path.starts_with("assets/") && !path.starts_with("favicon") && !path.contains('.') {
                let f = WebAssets::get("index.html")?;
                (f, "index.html")
            } else {
                return None;
            }
        }
    };

    // Guess MIME from the ACTUAL file being served, not the request path.
    // This ensures index.html (served via SPA fallback) gets text/html.
    let mime = mime_guess::from_path(served_path).first_or_octet_stream();

    let mut response = Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, mime.as_ref())
        .body(Body::from(file.data.into_owned()))
        .ok()?;

    // Set cache headers for hashed assets
    if served_path.starts_with("assets/") {
        let val = HeaderValue::from_static("public, max-age=31536000, immutable");
        response.headers_mut().insert(header::CACHE_CONTROL, val);
    }

    Some(response)
}

/// Serve an embedded file by path — stub when embedded-assets feature is off.
#[cfg(not(feature = "embedded-assets"))]
pub fn serve_embedded(_path: &str) -> Option<Response<Body>> {
    None
}

/// Check whether embedded assets are available.
#[cfg(feature = "embedded-assets")]
pub fn has_embedded() -> bool {
    WebAssets::iter().next().is_some()
}

/// Check whether embedded assets are available — stub when feature is off.
#[cfg(not(feature = "embedded-assets"))]
pub fn has_embedded() -> bool {
    false
}

/// Axum fallback handler that tries embedded assets first, then disk.
pub async fn embedded_fallback(uri: axum::http::Uri) -> impl IntoResponse {
    let path = uri.path();

    // 1. Try embedded assets
    if let Some(resp) = serve_embedded(path) {
        return resp;
    }

    // 2. Try disk-based serving (MADHYAMAS_WEB_DIR or web/dist)
    let web_dir = std::env::var("MADHYAMAS_WEB_DIR").unwrap_or_else(|_| "web/dist".to_string());
    let disk_path = format!("{}/{}", web_dir, path.trim_start_matches('/'));

    if let Ok(metadata) = std::fs::metadata(&disk_path) {
        if metadata.is_file() {
            if let Ok(data) = std::fs::read(&disk_path) {
                let mime = mime_guess::from_path(&disk_path).first_or_octet_stream();
                return Response::builder()
                    .status(StatusCode::OK)
                    .header(header::CONTENT_TYPE, mime.as_ref())
                    .body(Body::from(data))
                    .unwrap_or_else(|_| StatusCode::NOT_FOUND.into_response());
            }
        }
    }

    // 3. SPA fallback — try index.html from embedded or disk
    if let Some(resp) = serve_embedded("index.html") {
        return resp;
    }

    let index_path = format!("{}/index.html", web_dir);
    if let Ok(data) = std::fs::read(&index_path) {
        return Response::builder()
            .status(StatusCode::OK)
            .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
            .body(Body::from(data))
            .unwrap_or_else(|_| StatusCode::NOT_FOUND.into_response());
    }

    StatusCode::NOT_FOUND.into_response()
}