use std::sync::LazyLock;
use axum::Router;
use axum::extract::Path;
use axum::http::{HeaderMap, StatusCode, header};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
struct Asset {
name: &'static str,
mime: &'static str,
body: &'static [u8],
etag: LazyLock<String>,
}
macro_rules! asset {
($name:literal, $mime:literal) => {{
const BODY: &[u8] = include_bytes!(concat!("../ui/dist/", $name));
Asset {
name: $name,
mime: $mime,
body: BODY,
etag: LazyLock::new(|| etag(BODY)),
}
}};
}
static ASSETS: [Asset; 3] = [
asset!("index.html", "text/html; charset=utf-8"),
asset!("app.js", "text/javascript; charset=utf-8"),
asset!("app.css", "text/css; charset=utf-8"),
];
pub fn router() -> Router {
Router::new()
.route("/", get(index))
.route("/{file}", get(file))
}
async fn index(headers: HeaderMap) -> Response {
serve(&ASSETS[0], &headers)
}
async fn file(Path(file): Path<String>, headers: HeaderMap) -> Response {
match ASSETS.iter().find(|a| a.name == file) {
Some(a) => serve(a, &headers),
None => (StatusCode::NOT_FOUND, "not found\n").into_response(),
}
}
fn serve(a: &Asset, headers: &HeaderMap) -> Response {
let etag = a.etag.as_str();
let head = [
(header::CONTENT_TYPE, a.mime),
(header::ETAG, etag),
(header::CACHE_CONTROL, "no-cache"),
];
if headers
.get(header::IF_NONE_MATCH)
.and_then(|v| v.to_str().ok())
.is_some_and(|v| v.split(',').any(|t| t.trim() == etag))
{
return (StatusCode::NOT_MODIFIED, head).into_response();
}
(head, a.body).into_response()
}
fn etag(body: &[u8]) -> String {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in body {
h ^= *b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("\"{h:016x}\"")
}