use std::path::PathBuf;
use axum::Router as AXRouter;
use axum::{
body::Body,
extract::{Path as AxumPath, Request},
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result};
include!(concat!(env!("OUT_DIR"), "/generated_code/static_assets.rs"));
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct StaticAssets {
#[serde(default)]
pub enable: bool,
#[serde(default = "default_must_exist")]
pub must_exist: bool,
#[serde(default = "default_folder_config")]
pub folder: FolderConfig,
#[serde(default = "default_fallback")]
pub fallback: PathBuf,
#[serde(default = "default_precompressed")]
pub precompressed: bool,
}
impl Default for StaticAssets {
fn default() -> Self {
serde_json::from_value(json!({})).unwrap()
}
}
fn default_must_exist() -> bool {
true
}
fn default_precompressed() -> bool {
false
}
fn default_fallback() -> PathBuf {
PathBuf::from("assets").join("static").join("404.html")
}
fn default_folder_config() -> FolderConfig {
FolderConfig {
uri: "/static".to_string(),
path: PathBuf::from("assets/static"),
}
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct FolderConfig {
pub uri: String,
pub path: PathBuf,
}
#[derive(Clone)]
pub struct EmbeddedAssets {
fallback_content: &'static [u8],
}
impl EmbeddedAssets {
fn new(fallback_path: &str) -> Self {
tracing::info!(
"Initializing embedded static assets with fallback path: {}",
fallback_path
);
let assets = get_embedded_static_assets();
tracing::info!("Loaded {} embedded static assets", assets.len());
let available_files: Vec<String> = assets.keys().cloned().collect();
tracing::info!("Available embedded assets: {:?}", available_files);
let fallback = assets.get(fallback_path).copied().unwrap_or_else(|| {
tracing::warn!(
"Fallback file not found in embedded assets: {}",
fallback_path
);
let fallback_html = concat!(
"<!DOCTYPE html><html><body>",
"<h1>404 - Not Found</h1>",
"</body></html>"
);
fallback_html.as_bytes()
});
Self {
fallback_content: fallback,
}
}
fn serve(&self, uri: &str) -> impl IntoResponse {
let assets = get_embedded_static_assets();
assets.get(uri).map_or_else(
|| {
tracing::warn!("Static asset not found: {}, serving fallback", uri);
Response::builder()
.status(StatusCode::NOT_FOUND)
.header("content-type", "text/html")
.body(Body::from(self.fallback_content))
.unwrap()
},
|content| {
let content_type = match uri.rsplit('.').next() {
Some("css") => "text/css",
Some("js") => "application/javascript",
Some("html") => "text/html",
Some("png") => "image/png",
Some("jpg" | "jpeg") => "image/jpeg",
Some("svg") => "image/svg+xml",
Some("ico") => "image/x-icon",
Some("json") => "application/json",
Some("woff") => "font/woff",
Some("woff2") => "font/woff2",
Some("ttf") => "font/ttf",
Some("eot") => "application/vnd.ms-fontobject",
Some("otf") => "font/otf",
_ => "application/octet-stream",
};
tracing::debug!("Serving embedded static asset: {}", uri);
Response::builder()
.status(StatusCode::OK)
.header("content-type", content_type)
.body(Body::from(*content))
.unwrap()
},
)
}
}
impl MiddlewareLayer for StaticAssets {
fn name(&self) -> &'static str {
"static"
}
fn is_enabled(&self) -> bool {
self.enable
}
fn config(&self) -> serde_json::Result<serde_json::Value> {
serde_json::to_value(self)
}
fn apply(&self, app: AXRouter<AppContext>) -> Result<AXRouter<AppContext>> {
let fallback_path = format!(
"/{}",
self.fallback
.strip_prefix("assets")
.unwrap_or(&self.fallback)
.display()
.to_string()
.replace('\\', "/")
);
let embedded_assets = EmbeddedAssets::new(&fallback_path);
let base_uri = self.folder.uri.clone();
if &base_uri == "/" {
Ok(app.fallback(move |req: Request| {
let uri = req.uri().path().to_string();
let assets = embedded_assets.clone();
async move { assets.serve(&uri) }
}))
} else {
Ok(app.route(
&format!("{base_uri}/{{*path}}"),
get(move |AxumPath(path): AxumPath<String>| {
let uri = format!("{base_uri}/{path}");
let assets = embedded_assets.clone();
async move { assets.serve(&uri) }
}),
))
}
}
}