210 lines
6.9 KiB
Rust
210 lines
6.9 KiB
Rust
use axum::{
|
|
body::Body,
|
|
extract::{Path, State},
|
|
http::{HeaderName, HeaderValue, StatusCode, header},
|
|
response::{IntoResponse, Response},
|
|
};
|
|
use platform_oss::{LegacyAssetPrefix, OssSignedGetObjectUrlRequest};
|
|
use serde_json::json;
|
|
|
|
use crate::{http_error::AppError, state::AppState};
|
|
|
|
const CACHE_CONTROL_VALUE: &str = "private, max-age=60";
|
|
const ASSET_OBJECT_KEY_HEADER: &str = "x-genarrative-asset-object-key";
|
|
|
|
pub async fn proxy_generated_character_drafts(
|
|
State(state): State<AppState>,
|
|
Path(path): Path<String>,
|
|
) -> Response {
|
|
proxy_legacy_generated_asset(state, LegacyAssetPrefix::CharacterDrafts, path).await
|
|
}
|
|
|
|
pub async fn proxy_generated_characters(
|
|
State(state): State<AppState>,
|
|
Path(path): Path<String>,
|
|
) -> Response {
|
|
proxy_legacy_generated_asset(state, LegacyAssetPrefix::Characters, path).await
|
|
}
|
|
|
|
pub async fn proxy_generated_animations(
|
|
State(state): State<AppState>,
|
|
Path(path): Path<String>,
|
|
) -> Response {
|
|
proxy_legacy_generated_asset(state, LegacyAssetPrefix::Animations, path).await
|
|
}
|
|
|
|
pub async fn proxy_generated_custom_world_scenes(
|
|
State(state): State<AppState>,
|
|
Path(path): Path<String>,
|
|
) -> Response {
|
|
proxy_legacy_generated_asset(state, LegacyAssetPrefix::CustomWorldScenes, path).await
|
|
}
|
|
|
|
pub async fn proxy_generated_custom_world_covers(
|
|
State(state): State<AppState>,
|
|
Path(path): Path<String>,
|
|
) -> Response {
|
|
proxy_legacy_generated_asset(state, LegacyAssetPrefix::CustomWorldCovers, path).await
|
|
}
|
|
|
|
pub async fn proxy_generated_qwen_sprites(
|
|
State(state): State<AppState>,
|
|
Path(path): Path<String>,
|
|
) -> Response {
|
|
proxy_legacy_generated_asset(state, LegacyAssetPrefix::QwenSprites, path).await
|
|
}
|
|
|
|
async fn proxy_legacy_generated_asset(
|
|
state: AppState,
|
|
prefix: LegacyAssetPrefix,
|
|
path: String,
|
|
) -> Response {
|
|
match read_legacy_generated_asset(&state, prefix, path).await {
|
|
Ok(response) => response,
|
|
Err(error) => error.into_response(),
|
|
}
|
|
}
|
|
|
|
async fn read_legacy_generated_asset(
|
|
state: &AppState,
|
|
prefix: LegacyAssetPrefix,
|
|
path: String,
|
|
) -> Result<Response, AppError> {
|
|
let oss_client = state.oss_client().ok_or_else(|| {
|
|
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
|
|
"provider": "aliyun-oss",
|
|
"reason": "OSS 未完成环境变量配置",
|
|
}))
|
|
})?;
|
|
let object_key = build_generated_object_key(prefix, path.as_str())?;
|
|
let signed = oss_client
|
|
.sign_get_object_url(OssSignedGetObjectUrlRequest {
|
|
object_key: object_key.clone(),
|
|
expire_seconds: Some(60),
|
|
})
|
|
.map_err(map_legacy_generated_oss_error)?;
|
|
let upstream_response = reqwest::Client::new()
|
|
.get(signed.signed_url)
|
|
.send()
|
|
.await
|
|
.map_err(|error| {
|
|
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
|
"provider": "aliyun-oss",
|
|
"message": format!("读取 OSS 旧 generated 资源失败:{error}"),
|
|
}))
|
|
})?;
|
|
|
|
if upstream_response.status() == reqwest::StatusCode::NOT_FOUND {
|
|
return Err(
|
|
AppError::from_status(StatusCode::NOT_FOUND).with_details(json!({
|
|
"provider": "aliyun-oss",
|
|
"objectKey": object_key,
|
|
})),
|
|
);
|
|
}
|
|
|
|
let status = upstream_response.status();
|
|
let content_type = upstream_response
|
|
.headers()
|
|
.get(header::CONTENT_TYPE)
|
|
.cloned();
|
|
let bytes = upstream_response
|
|
.error_for_status()
|
|
.map_err(|error| {
|
|
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
|
"provider": "aliyun-oss",
|
|
"message": format!("读取 OSS 旧 generated 资源失败:{error}"),
|
|
}))
|
|
})?
|
|
.bytes()
|
|
.await
|
|
.map_err(|error| {
|
|
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
|
"provider": "aliyun-oss",
|
|
"message": format!("读取 OSS 旧 generated 资源内容失败:{error}"),
|
|
}))
|
|
})?;
|
|
let mut response = Response::builder()
|
|
.status(status)
|
|
.header(header::CACHE_CONTROL, CACHE_CONTROL_VALUE)
|
|
.header(
|
|
HeaderName::from_static(ASSET_OBJECT_KEY_HEADER),
|
|
HeaderValue::from_str(object_key.as_str()).map_err(|error| {
|
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
|
"provider": "legacy-generated-assets",
|
|
"message": format!("构造资源响应头失败:{error}"),
|
|
}))
|
|
})?,
|
|
);
|
|
if let Some(content_type) = content_type {
|
|
response = response.header(header::CONTENT_TYPE, content_type);
|
|
}
|
|
|
|
response.body(Body::from(bytes)).map_err(|error| {
|
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
|
"provider": "legacy-generated-assets",
|
|
"message": format!("构造资源响应失败:{error}"),
|
|
}))
|
|
})
|
|
}
|
|
|
|
fn build_generated_object_key(prefix: LegacyAssetPrefix, path: &str) -> Result<String, AppError> {
|
|
let path = path.trim().trim_matches('/');
|
|
if path.is_empty() || path.split('/').any(is_invalid_path_segment) {
|
|
return Err(
|
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
|
"provider": "legacy-generated-assets",
|
|
"message": "generated 资源路径不合法。",
|
|
})),
|
|
);
|
|
}
|
|
|
|
Ok(format!("{}/{}", prefix.as_str(), path))
|
|
}
|
|
|
|
fn is_invalid_path_segment(segment: &str) -> bool {
|
|
segment.is_empty() || segment == "." || segment == ".." || segment.contains('\\')
|
|
}
|
|
|
|
fn map_legacy_generated_oss_error(error: platform_oss::OssError) -> AppError {
|
|
let status = match error {
|
|
platform_oss::OssError::InvalidConfig(_) | platform_oss::OssError::InvalidRequest(_) => {
|
|
StatusCode::BAD_REQUEST
|
|
}
|
|
platform_oss::OssError::ObjectNotFound(_) => StatusCode::NOT_FOUND,
|
|
platform_oss::OssError::Request(_)
|
|
| platform_oss::OssError::SerializePolicy(_)
|
|
| platform_oss::OssError::Sign(_) => StatusCode::BAD_GATEWAY,
|
|
};
|
|
AppError::from_status(status).with_details(json!({
|
|
"provider": "aliyun-oss",
|
|
"message": error.to_string(),
|
|
}))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn build_generated_object_key_keeps_supported_prefix() {
|
|
let object_key = build_generated_object_key(
|
|
LegacyAssetPrefix::Animations,
|
|
"hero/animation-set-1/idle/frame01.png",
|
|
)
|
|
.expect("object key should build");
|
|
|
|
assert_eq!(
|
|
object_key,
|
|
"generated-animations/hero/animation-set-1/idle/frame01.png"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn build_generated_object_key_rejects_parent_segment() {
|
|
assert!(
|
|
build_generated_object_key(LegacyAssetPrefix::Characters, "../secret.png").is_err()
|
|
);
|
|
}
|
|
}
|