Files
psg-backchannel/backchannel-server/src/http/server.rs
2026-02-19 13:51:07 +08:00

66 lines
1.9 KiB
Rust

use anyhow::{Context, Result};
use axum::body::Body;
use axum::extract::Path;
use axum::http::{header, HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::Router;
use mime_guess::from_path;
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "../backchannel-web/dist"]
struct WebAssets;
pub async fn serve(bind_addr: &str) -> Result<()> {
let app = Router::new()
.route("/", get(index))
.route("/*path", get(asset_or_index));
let listener = tokio::net::TcpListener::bind(bind_addr)
.await
.with_context(|| format!("Failed to bind HTTP server to {bind_addr}"))?;
tracing::info!("BackChannel web UI listening on http://{bind_addr}");
axum::serve(listener, app).await.context("HTTP server failed")
}
async fn index() -> Response {
render_asset("index.html")
}
async fn asset_or_index(Path(path): Path<String>) -> Response {
if path.is_empty() {
return render_asset("index.html");
}
render_asset(&path)
}
fn render_asset(path: &str) -> Response {
let canonical = path.trim_start_matches('/');
if let Some(content) = WebAssets::get(canonical) {
return build_response(canonical, content.data.as_ref(), StatusCode::OK);
}
if let Some(index) = WebAssets::get("index.html") {
return build_response("index.html", index.data.as_ref(), StatusCode::OK);
}
(StatusCode::NOT_FOUND, "Web UI asset not found").into_response()
}
fn build_response(path: &str, body: &[u8], status: StatusCode) -> Response {
let mime = from_path(path).first_or_octet_stream();
let mut response = Response::new(Body::from(body.to_vec()));
*response.status_mut() = status;
response.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_str(mime.as_ref()).unwrap_or(HeaderValue::from_static("application/octet-stream")),
);
response
}