feat: serve website at root path

This commit is contained in:
boos
2026-06-06 05:59:48 +08:00
parent 03046152fd
commit 5ef017c54e
6 changed files with 155 additions and 60 deletions
+86 -25
View File
@@ -13,6 +13,7 @@ use tower::service_fn;
use tower_http::services::ServeDir;
static EMBEDDED_WEB: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/dist/web");
static EMBEDDED_WEBSITE: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/dist/website");
mod admin;
mod auth;
@@ -82,28 +83,28 @@ pub fn build_router(state: AppState) -> Router {
.filter(|value| !value.is_empty())
{
Some(web_dir) => {
let web_dir_owned = web_dir.clone();
let website_dir = std::env::var("BOOSAPI_WEBSITE_DIR")
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "dist/website".to_string());
let static_service = ServeDir::new(&web_dir)
.append_index_html_on_directories(true)
.fallback(service_fn(move |req: Request<Body>| {
let web_dir = web_dir_owned.clone();
let web_dir = web_dir.clone();
let website_dir = website_dir.clone();
async move {
let path = req.uri().path();
if is_api_path(path) {
return Ok::<_, Infallible>(api_not_found_response(path));
}
let index_path = format!("{}/index.html", web_dir);
match tokio::fs::read_to_string(&index_path).await {
Ok(html) => Ok::<_, Infallible>(html_response(html)),
Err(_) => Ok::<_, Infallible>(
Response::builder()
.status(StatusCode::NOT_FOUND)
.header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
.body(Body::from(format!("404: {} not found", path)))
.unwrap_or_else(|_| StatusCode::NOT_FOUND.into_response()),
),
}
Ok::<_, Infallible>(filesystem_static_response(
path,
&web_dir,
&website_dir,
)
.await)
}
}));
router.fallback_service(static_service)
@@ -122,6 +123,49 @@ fn api_not_found_response(path: &str) -> Response {
.unwrap_or_else(|_| StatusCode::NOT_FOUND.into_response())
}
async fn filesystem_static_response(path: &str, web_dir: &str, website_dir: &str) -> Response {
let root_dir = if should_serve_website(path) {
website_dir
} else {
web_dir
};
let asset_path = embedded_asset_path(path);
let candidate = format!("{}/{}", root_dir.trim_end_matches('/'), asset_path);
match tokio::fs::read(&candidate).await {
Ok(bytes) => return embedded_file_response(&asset_path, &bytes),
Err(_) => {
if candidate.ends_with("/index.html") {
let without_index = candidate.trim_end_matches("index.html");
if let Ok(bytes) = tokio::fs::read(without_index).await {
return embedded_file_response(&asset_path, &bytes);
}
}
}
}
let index_path = format!("{}/index.html", root_dir.trim_end_matches('/'));
match tokio::fs::read(&index_path).await {
Ok(bytes) => embedded_file_response("index.html", &bytes),
Err(_) if should_serve_website(path) => {
let fallback_index = format!("{}/index.html", web_dir.trim_end_matches('/'));
match tokio::fs::read(&fallback_index).await {
Ok(bytes) => embedded_file_response("index.html", &bytes),
Err(_) => not_found_response(path),
}
}
Err(_) => not_found_response(path),
}
}
fn not_found_response(path: &str) -> Response {
Response::builder()
.status(StatusCode::NOT_FOUND)
.header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
.body(Body::from(format!("404: {} not found", path)))
.unwrap_or_else(|_| StatusCode::NOT_FOUND.into_response())
}
async fn embedded_static_handler(req: Request<Body>) -> Response {
let path = req.uri().path();
if is_api_path(path) {
@@ -129,23 +173,48 @@ async fn embedded_static_handler(req: Request<Body>) -> Response {
}
let asset_path = embedded_asset_path(path);
if let Some(file) = EMBEDDED_WEB.get_file(&asset_path) {
let static_dir = if should_serve_website(path) {
&EMBEDDED_WEBSITE
} else {
&EMBEDDED_WEB
};
if let Some(file) = static_dir.get_file(&asset_path) {
return embedded_file_response(&asset_path, file.contents());
}
if let Some(file) = EMBEDDED_WEB.get_file("index.html") {
if let Some(file) = static_dir.get_file("index.html") {
return embedded_file_response("index.html", file.contents());
}
if should_serve_website(path) && EMBEDDED_WEBSITE.get_file("index.html").is_none() {
if let Some(file) = EMBEDDED_WEB.get_file("index.html") {
return embedded_file_response("index.html", file.contents());
}
}
Response::builder()
.status(StatusCode::NOT_FOUND)
.header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
.body(Body::from("embedded dist/web/index.html not found"))
.body(Body::from("embedded frontend index.html not found"))
.unwrap_or_else(|_| StatusCode::NOT_FOUND.into_response())
}
fn is_api_path(path: &str) -> bool {
path.starts_with("/api/") || path.starts_with("/v1/")
path.starts_with("/api/")
|| path.starts_with("/v1/")
|| path == "/health"
|| path == "/ready"
|| path == "/metrics"
}
fn should_serve_website(path: &str) -> bool {
path == "/"
|| path.starts_with("/_next/")
|| path == "/favicon.ico"
|| path == "/favicon.png"
|| path == "/favicon-64.png"
|| path == "/logo.png"
}
fn embedded_asset_path(path: &str) -> String {
@@ -157,14 +226,6 @@ fn embedded_asset_path(path: &str) -> String {
}
}
fn html_response(html: String) -> Response {
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "text/html; charset=utf-8")
.body(Body::from(html))
.unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
}
fn embedded_file_response(path: &str, bytes: &[u8]) -> Response {
let content_type = mime_guess::from_path(path)
.first_or_octet_stream()