Update: 将子项目从 submodule 转为完整内容

- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
This commit is contained in:
freedak
2026-07-04 19:20:46 +08:00
parent 54d6465fa7
commit f7a720204a
3360 changed files with 802660 additions and 3 deletions
@@ -0,0 +1,8 @@
//! Backend-served static logo assets.
pub mod routes;
pub mod service;
pub mod state;
pub use routes::asset_routes;
pub use service::AssetService;
pub use state::AssetRouterState;
@@ -0,0 +1,138 @@
use axum::Router;
use axum::body::Body;
use axum::extract::{Path, State};
use axum::http::{HeaderMap, StatusCode, header};
use axum::response::Response;
use axum::routing::get;
use nomifun_common::AppError;
use crate::state::AssetRouterState;
const CACHE_CONTROL_VALUE: &str = "public, max-age=31536000, immutable";
/// Build the public `/api/assets/*` router.
pub fn asset_routes(state: AssetRouterState) -> Router {
Router::new()
.route("/api/assets/logos/{*asset_path}", get(get_logo_asset))
.with_state(state)
}
async fn get_logo_asset(
State(state): State<AssetRouterState>,
Path(asset_path): Path<String>,
headers: HeaderMap,
) -> Result<Response, AppError> {
let asset = state.service.get_logo(&asset_path)?;
if state
.service
.etag_matches(headers.get(header::IF_NONE_MATCH), &asset.etag)
{
return Response::builder()
.status(StatusCode::NOT_MODIFIED)
.header(header::CACHE_CONTROL, CACHE_CONTROL_VALUE)
.header(header::ETAG, asset.etag)
.body(Body::empty())
.map_err(|error| AppError::Internal(error.to_string()));
}
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, asset.content_type)
.header(header::CACHE_CONTROL, CACHE_CONTROL_VALUE)
.header(header::ETAG, asset.etag)
.body(Body::from(asset.bytes))
.map_err(|error| AppError::Internal(error.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::Request;
use http_body_util::BodyExt;
use tower::ServiceExt;
#[tokio::test]
async fn get_logo_asset_serves_embedded_logo() {
let router = asset_routes(AssetRouterState::default());
let response = router
.oneshot(
Request::builder()
.uri("/api/assets/logos/ai-major/claude.svg")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.headers()[header::CONTENT_TYPE], "image/svg+xml");
assert_eq!(response.headers()[header::CACHE_CONTROL], CACHE_CONTROL_VALUE);
assert!(response.headers().contains_key(header::ETAG));
assert!(!response.into_body().collect().await.unwrap().to_bytes().is_empty());
}
#[tokio::test]
async fn get_logo_asset_returns_not_modified_for_matching_etag() {
let router = asset_routes(AssetRouterState::default());
let first = router
.clone()
.oneshot(
Request::builder()
.uri("/api/assets/logos/ai-major/claude.svg")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let etag = first.headers()[header::ETAG].clone();
let response = router
.oneshot(
Request::builder()
.uri("/api/assets/logos/ai-major/claude.svg")
.header(header::IF_NONE_MATCH, etag)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
assert_eq!(response.headers()[header::CACHE_CONTROL], CACHE_CONTROL_VALUE);
assert_eq!(response.into_body().collect().await.unwrap().to_bytes().len(), 0);
}
#[tokio::test]
async fn get_logo_asset_rejects_traversal() {
let router = asset_routes(AssetRouterState::default());
let response = router
.oneshot(
Request::builder()
.uri("/api/assets/logos/%2E%2E%2Fbrand%2Fnomi.svg")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn get_logo_asset_returns_not_found_for_missing_file() {
let router = asset_routes(AssetRouterState::default());
let response = router
.oneshot(
Request::builder()
.uri("/api/assets/logos/ai-major/missing.svg")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
}
@@ -0,0 +1,128 @@
use std::path::{Component, Path, PathBuf};
use http::HeaderValue;
use nomifun_common::AppError;
use rust_embed::RustEmbed;
use sha2::{Digest, Sha256};
#[derive(RustEmbed)]
#[folder = "assets/logos/"]
struct LogoAssets;
/// Resolved static asset bytes plus cache metadata.
pub struct AssetFile {
pub bytes: Vec<u8>,
pub content_type: HeaderValue,
pub etag: HeaderValue,
}
/// Service resolving embedded logo assets.
#[derive(Clone, Default)]
pub struct AssetService;
impl AssetService {
/// Look up a logo asset by its route-relative path.
pub fn get_logo(&self, asset_path: &str) -> Result<AssetFile, AppError> {
let normalized = normalize_logo_path(asset_path)
.ok_or_else(|| AppError::Forbidden(format!("Asset path escapes logos root: {asset_path}")))?;
let file = LogoAssets::get(&normalized).ok_or_else(|| AppError::NotFound("Logo asset not found".into()))?;
let bytes = file.data.into_owned();
Ok(AssetFile {
content_type: content_type_for_path(&normalized),
etag: build_etag(&bytes)?,
bytes,
})
}
/// Return `true` when the request ETag already matches the asset.
pub fn etag_matches(&self, header_value: Option<&HeaderValue>, etag: &HeaderValue) -> bool {
let Some(header_value) = header_value else {
return false;
};
let Ok(expected) = etag.to_str() else {
return false;
};
let Ok(candidate) = header_value.to_str() else {
return false;
};
candidate
.split(',')
.map(str::trim)
.any(|value| value == "*" || value == expected)
}
}
fn normalize_logo_path(path: &str) -> Option<String> {
if path.contains('\\') || path.contains(':') {
return None;
}
let mut normalized = PathBuf::new();
for component in Path::new(path).components() {
match component {
Component::Normal(value) => normalized.push(value),
Component::CurDir => {}
Component::RootDir | Component::ParentDir | Component::Prefix(_) => return None,
}
}
if normalized.as_os_str().is_empty() {
return None;
}
Some(normalized.to_string_lossy().replace('\\', "/"))
}
fn content_type_for_path(path: &str) -> HeaderValue {
let mime = mime_guess::from_path(path).first_or_octet_stream();
HeaderValue::from_str(mime.as_ref()).unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream"))
}
fn build_etag(bytes: &[u8]) -> Result<HeaderValue, AppError> {
let digest = Sha256::digest(bytes);
HeaderValue::from_str(&format!("\"{digest:x}\"")).map_err(|error| AppError::Internal(error.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_logo_path_rejects_traversal() {
assert!(normalize_logo_path("../brand/nomi.svg").is_none());
assert!(normalize_logo_path("/etc/passwd").is_none());
assert!(normalize_logo_path("C:\\Windows\\System32").is_none());
}
#[test]
fn normalize_logo_path_preserves_nested_relative_paths() {
assert_eq!(
normalize_logo_path("./ai-major/claude.svg").as_deref(),
Some("ai-major/claude.svg")
);
}
#[test]
fn get_logo_returns_bytes_and_metadata() {
let service = AssetService;
let asset = service.get_logo("ai-major/claude.svg").expect("claude logo present");
assert_eq!(asset.content_type, HeaderValue::from_static("image/svg+xml"));
assert!(!asset.bytes.is_empty());
assert!(asset.etag.to_str().unwrap().starts_with('"'));
}
#[test]
fn etag_matches_supports_exact_and_star_values() {
let service = AssetService;
let etag = HeaderValue::from_static("\"abc\"");
assert!(service.etag_matches(Some(&HeaderValue::from_static("\"abc\"")), &etag));
assert!(service.etag_matches(Some(&HeaderValue::from_static("*")), &etag));
assert!(service.etag_matches(Some(&HeaderValue::from_static("\"def\", \"abc\"")), &etag));
assert!(!service.etag_matches(Some(&HeaderValue::from_static("\"def\"")), &etag));
}
}
@@ -0,0 +1,17 @@
use std::sync::Arc;
use crate::service::AssetService;
/// Shared state for the public asset router.
#[derive(Clone)]
pub struct AssetRouterState {
pub service: Arc<AssetService>,
}
impl Default for AssetRouterState {
fn default() -> Self {
Self {
service: Arc::new(AssetService),
}
}
}