feat: 允许通过环境变量手动指定前端资源路径,删除 debug 分支默认资源路径

This commit is contained in:
mofeng-git
2026-05-04 17:53:27 +08:00
parent 12a3f1c947
commit 6723f432a3
2 changed files with 71 additions and 71 deletions

View File

@@ -49,7 +49,7 @@ reqwest = { version = "0.13", features = ["stream", "rustls", "json"], default-f
urlencoding = "2" urlencoding = "2"
# Static file embedding # Static file embedding
rust-embed = { version = "8", features = ["compression"] } rust-embed = { version = "8", features = ["compression", "debug-embed"] }
mime_guess = "2" mime_guess = "2"
# TLS/HTTPS # TLS/HTTPS

View File

@@ -4,31 +4,40 @@ use axum::{
routing::get, routing::get,
Router, Router,
}; };
#[cfg(debug_assertions)] use rust_embed::Embed;
use std::path::PathBuf; use std::path::{Path, PathBuf};
#[cfg(debug_assertions)]
use std::sync::OnceLock; use std::sync::OnceLock;
#[cfg(not(debug_assertions))] const FRONTEND_DIR_ENV: &str = "ONE_KVM_FRONTEND_DIR";
use rust_embed::Embed;
#[cfg(not(debug_assertions))]
#[derive(Embed)] #[derive(Embed)]
#[folder = "web/dist"] #[folder = "web/dist"]
#[prefix = ""] #[prefix = ""]
pub struct StaticAssets; pub struct StaticAssets;
#[cfg(debug_assertions)] fn frontend_dir_override() -> Option<PathBuf> {
fn get_static_base_dir() -> PathBuf { static FRONTEND_DIR: OnceLock<Option<PathBuf>> = OnceLock::new();
static BASE_DIR: OnceLock<PathBuf> = OnceLock::new(); FRONTEND_DIR
BASE_DIR
.get_or_init(|| { .get_or_init(|| {
if let Ok(exe_path) = std::env::current_exe() { let value = std::env::var_os(FRONTEND_DIR_ENV)?;
if let Some(exe_dir) = exe_path.parent() { let path = PathBuf::from(value);
return exe_dir.join("web").join("dist");
if path.as_os_str().is_empty() {
return None;
}
match path.canonicalize() {
Ok(path) => Some(path),
Err(e) => {
tracing::warn!(
"{}='{}' is not accessible: {}",
FRONTEND_DIR_ENV,
path.display(),
e
);
None
} }
} }
PathBuf::from("web/dist")
}) })
.clone() .clone()
} }
@@ -84,71 +93,62 @@ fn serve_file(path: &str) -> Response<Body> {
} }
fn try_serve_file(path: &str) -> Option<Response<Body>> { fn try_serve_file(path: &str) -> Option<Response<Body>> {
#[cfg(debug_assertions)] if let Some(base_dir) = frontend_dir_override() {
{ return try_serve_file_from_dir(&base_dir, path);
let base_dir = get_static_base_dir(); }
let file_path = base_dir.join(path);
if !file_path.starts_with(&base_dir) { let asset = StaticAssets::get(path)?;
tracing::warn!("Path traversal attempt blocked: {}", path); Some(static_response(path, asset.data.to_vec()))
}
fn try_serve_file_from_dir(base_dir: &Path, path: &str) -> Option<Response<Body>> {
let file_path = base_dir.join(path);
let normalized_path = match file_path.canonicalize() {
Ok(path) => path,
Err(e) => {
tracing::debug!(
"Failed to resolve static file '{}' from '{}': {}",
path,
file_path.display(),
e
);
return None; return None;
} }
};
if let (Ok(normalized_path), Ok(normalized_base)) = if !normalized_path.starts_with(base_dir) {
(file_path.canonicalize(), base_dir.canonicalize()) tracing::warn!("Path traversal attempt blocked: {}", path);
{ return None;
if !normalized_path.starts_with(&normalized_base) {
tracing::warn!("Path traversal attempt blocked (canonicalized): {}", path);
return None;
}
}
match std::fs::read(&file_path) {
Ok(data) => {
let mime = mime_guess::from_path(path)
.first_or_octet_stream()
.to_string();
Some(
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, mime)
.header(header::CACHE_CONTROL, "public, max-age=86400")
.body(Body::from(data))
.unwrap(),
)
}
Err(e) => {
tracing::debug!(
"Failed to read static file '{}' from '{}': {}",
path,
file_path.display(),
e
);
None
}
}
} }
#[cfg(not(debug_assertions))] match std::fs::read(&normalized_path) {
{ Ok(data) => Some(static_response(path, data)),
let asset = StaticAssets::get(path)?; Err(e) => {
tracing::debug!(
let mime = mime_guess::from_path(path) "Failed to read static file '{}' from '{}': {}",
.first_or_octet_stream() path,
.to_string(); normalized_path.display(),
e
Some( );
Response::builder() None
.status(StatusCode::OK) }
.header(header::CONTENT_TYPE, mime)
.header(header::CACHE_CONTROL, "public, max-age=86400")
.body(Body::from(asset.data.to_vec()))
.unwrap(),
)
} }
} }
fn static_response(path: &str, data: Vec<u8>) -> Response<Body> {
let mime = mime_guess::from_path(path)
.first_or_octet_stream()
.to_string();
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, mime)
.header(header::CACHE_CONTROL, "public, max-age=86400")
.body(Body::from(data))
.unwrap()
}
pub fn placeholder_html() -> &'static str { pub fn placeholder_html() -> &'static str {
r#"<!DOCTYPE html> r#"<!DOCTYPE html>
<html lang="en"> <html lang="en">