提交qiming-mcp-proxy

This commit is contained in:
Codex
2026-06-01 13:03:20 +08:00
parent 9e9486b7c2
commit afb3d9f4e6
394 changed files with 124494 additions and 0 deletions

View File

@@ -0,0 +1,486 @@
use axum::{
extract::{Multipart, Query, State},
response::IntoResponse,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tracing::{error, info, warn};
use utoipa::ToSchema;
use crate::app_state::AppState;
use crate::handlers::response::ApiResponse;
/// 文件上传响应
#[derive(Debug, Serialize, ToSchema)]
pub struct FileUploadResponse {
pub oss_file_name: String,
pub oss_bucket: String,
pub download_url: String,
pub expires_in_hours: u64,
pub file_size: Option<u64>,
pub original_filename: Option<String>,
}
/// 下载URL响应
#[derive(Debug, Serialize, ToSchema)]
pub struct DownloadUrlResponse {
pub download_url: String,
pub oss_file_name: String,
pub oss_bucket: String,
pub expires_in_hours: u64,
}
/// 获取下载URL请求参数
#[derive(Debug, Deserialize, ToSchema)]
pub struct GetDownloadUrlParams {
pub file_name: String,
pub bucket: Option<String>,
}
/// 获取上传签名URL请求参数
#[derive(Debug, Deserialize, ToSchema)]
pub struct GetUploadSignUrlParams {
pub file_name: String,
pub content_type: Option<String>,
pub bucket: Option<String>,
}
/// 获取下载签名URL请求参数4小时有效
#[derive(Debug, Deserialize, ToSchema)]
pub struct GetDownloadSignUrlParams {
pub file_name: String,
pub bucket: Option<String>,
}
/// 删除文件请求参数
#[derive(Debug, Deserialize, ToSchema)]
pub struct DeleteFileParams {
pub file_name: String,
pub bucket: Option<String>,
}
/// 上传签名URL响应
#[derive(Debug, Serialize, ToSchema)]
pub struct UploadSignUrlResponse {
pub upload_url: String,
pub oss_file_name: String,
pub oss_bucket: String,
pub expires_in_hours: u64,
pub content_type: String,
}
/// 下载签名URL响应
#[derive(Debug, Serialize, ToSchema)]
pub struct DownloadSignUrlResponse {
pub download_url: String,
pub oss_file_name: String,
pub oss_bucket: String,
pub expires_in_hours: u64,
}
/// 删除文件响应
#[derive(Debug, Serialize, ToSchema)]
pub struct DeleteFileResponse {
pub oss_file_name: String,
pub oss_bucket: String,
pub message: String,
}
/// 上传文件到OSS
#[utoipa::path(
post,
path = "/api/v1/oss/upload",
request_body(content = String, description = "文件内容", content_type = "multipart/form-data"),
responses(
(status = 200, description = "上传成功", body = FileUploadResponse),
(status = 400, description = "请求参数错误"),
(status = 413, description = "文件过大"),
(status = 500, description = "服务器内部错误")
),
tag = "oss"
)]
pub async fn upload_file_to_oss(
State(state): State<AppState>,
mut multipart: Multipart,
) -> impl IntoResponse {
info!("OSS file upload request");
// 检查OSS客户端是否可用
let oss_client = match &state.private_oss_client {
Some(client) => client,
None => {
error!("The OSS client is not configured");
return ApiResponse::internal_error::<FileUploadResponse>("OSS客户端未配置")
.into_response();
}
};
let mut file_path: Option<String> = None;
let mut original_filename: Option<String> = None;
let mut temp_files = Vec::new();
// 处理multipart数据
while let Some(field) = multipart.next_field().await.unwrap_or(None) {
let field_name = field.name().unwrap_or("").to_string();
if field_name == "file" {
let filename = field.file_name().map(|s| s.to_string());
original_filename = filename.clone();
let data = match field.bytes().await {
Ok(data) => data,
Err(e) => {
error!("Failed to read file data: {}", e);
return ApiResponse::validation_error::<FileUploadResponse>("文件数据读取失败")
.into_response();
}
};
// 创建临时文件
let temp_file = match tempfile::NamedTempFile::new() {
Ok(file) => file,
Err(e) => {
error!("Failed to create temporary file: {}", e);
return ApiResponse::internal_error::<FileUploadResponse>("临时文件创建失败")
.into_response();
}
};
if let Err(e) = std::fs::write(temp_file.path(), &data) {
error!("Failed to write to temporary file: {}", e);
return ApiResponse::internal_error::<FileUploadResponse>("文件写入失败")
.into_response();
}
file_path = Some(temp_file.path().to_string_lossy().to_string());
temp_files.push(temp_file);
}
}
let file_path = match file_path {
Some(path) => path,
None => {
warn!("Uploaded file not found");
return ApiResponse::validation_error::<FileUploadResponse>("未提供文件")
.into_response();
}
};
// 获取文件大小
let file_size = std::fs::metadata(&file_path).map(|m| m.len()).ok();
// 生成对象键名
let object_key = if let Some(ref filename) = original_filename {
// 使用原始文件名,添加时间戳避免冲突
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S").to_string();
let clean_filename = oss_client::utils::sanitize_filename(filename);
format!("uploads/{timestamp}_{clean_filename}")
} else {
// 生成唯一文件名
format!(
"uploads/{}",
oss_client::utils::generate_random_filename(None)
)
};
// 上传到OSS
match oss_client.upload_file(&file_path, &object_key).await {
Ok(oss_url) => {
info!("File upload to OSS successful: object_key={}", object_key);
// 生成下载URL4小时有效
let download_url = match oss_client
.generate_download_url(&object_key, Some(Duration::from_secs(4 * 3600)))
{
Ok(url) => url,
Err(_) => oss_url.clone(), // 如果生成签名URL失败使用原始URL
};
let response = FileUploadResponse {
oss_file_name: object_key,
oss_bucket: oss_client.get_config().bucket.clone(),
download_url,
expires_in_hours: 4,
file_size,
original_filename,
};
ApiResponse::success(response).into_response()
}
Err(e) => {
error!("File upload to OSS failed: {}", e);
ApiResponse::internal_error::<FileUploadResponse>(&format!("上传失败: {e}"))
.into_response()
}
}
}
/// 获取上传签名URL
///
/// ⚠️ **重要警告**: 使用相同的文件名上传会完全覆盖OSS中的现有文件此操作不可逆
/// 建议在文件名中添加时间戳或UUID来避免意外覆盖例如document_20240101_120000.pdf
#[utoipa::path(
get,
path = "/api/v1/oss/upload-sign-url",
params(
("file_name" = String, Query, description = "文件名(⚠️警告:相同文件名会覆盖现有文件!建议添加时间戳避免覆盖)"),
("content_type" = Option<String>, Query, description = "文件内容类型,默认为 application/octet-stream"),
("bucket" = Option<String>, Query, description = "存储桶名称(可选)")
),
responses(
(status = 200, description = "获取成功返回4小时有效的上传签名URL", body = UploadSignUrlResponse),
(status = 400, description = "请求参数错误(如文件名为空)"),
(status = 500, description = "服务器内部错误如OSS客户端未配置")
),
tag = "oss"
)]
pub async fn get_upload_sign_url(
State(state): State<AppState>,
Query(params): Query<GetUploadSignUrlParams>,
) -> impl IntoResponse {
info!(
"Get the upload signature URL request: file_name={}, content_type={:?}, bucket={:?}",
params.file_name, params.content_type, params.bucket
);
// 验证文件名
if params.file_name.trim().is_empty() {
return ApiResponse::validation_error::<UploadSignUrlResponse>("文件名不能为空")
.into_response();
}
// 检查OSS客户端是否可用
let oss_client = match &state.private_oss_client {
Some(client) => client,
None => {
error!("The OSS client is not configured");
return ApiResponse::internal_error::<UploadSignUrlResponse>("OSS客户端未配置")
.into_response();
}
};
// 默认内容类型
let content_type = params
.content_type
.as_deref()
.unwrap_or("application/octet-stream");
// 4小时有效期
let expires_in = Duration::from_secs(4 * 3600);
// 构建完整的对象键名包含edu前缀
let object_key = if params.file_name.starts_with("edu/") {
// 如果已经包含前缀,直接使用
params.file_name.clone()
} else {
// 添加edu前缀
format!("edu/{}", params.file_name.trim_start_matches('/'))
};
// 生成上传签名URL
match oss_client.generate_upload_url(&object_key, expires_in, Some(content_type)) {
Ok(upload_url) => {
info!(
"Successfully generated upload signature URL: object_key={}, content_type={}",
object_key, content_type
);
let response = UploadSignUrlResponse {
upload_url,
oss_file_name: object_key,
oss_bucket: oss_client.get_config().bucket.clone(),
expires_in_hours: 4,
content_type: content_type.to_string(),
};
ApiResponse::success(response).into_response()
}
Err(e) => {
error!(
"Failed to generate upload signature URL: file_name={}, error={}",
params.file_name, e
);
ApiResponse::internal_error::<UploadSignUrlResponse>(&format!(
"生成上传签名URL失败: {e}"
))
.into_response()
}
}
}
/// 获取下载签名URL4小时有效
#[utoipa::path(
get,
path = "/api/v1/oss/download-sign-url",
params(
("file_name" = String, Query, description = "文件名"),
("bucket" = Option<String>, Query, description = "存储桶名称")
),
responses(
(status = 200, description = "获取成功", body = DownloadSignUrlResponse),
(status = 400, description = "请求参数错误"),
(status = 404, description = "文件不存在"),
(status = 500, description = "服务器内部错误")
),
tag = "oss"
)]
pub async fn get_download_sign_url(
State(state): State<AppState>,
Query(params): Query<GetDownloadSignUrlParams>,
) -> impl IntoResponse {
info!(
"Get download signature URL request: file_name={}, bucket={:?}",
params.file_name, params.bucket
);
// 验证文件名
if params.file_name.trim().is_empty() {
return ApiResponse::validation_error::<DownloadSignUrlResponse>("文件名不能为空")
.into_response();
}
// 检查OSS客户端是否可用
let oss_client = match &state.private_oss_client {
Some(client) => client,
None => {
error!("The OSS client is not configured");
return ApiResponse::internal_error::<DownloadSignUrlResponse>("OSS客户端未配置")
.into_response();
}
};
// 4小时有效期
let expires_in = Duration::from_secs(4 * 3600);
// 构建完整的对象键名如果没有前缀则添加edu前缀
let object_key = if params.file_name.starts_with("edu/") {
// 如果已经包含前缀,直接使用
params.file_name.clone()
} else {
// 添加edu前缀
format!("edu/{}", params.file_name.trim_start_matches('/'))
};
// 生成下载签名URL
match oss_client.generate_download_url(&object_key, Some(expires_in)) {
Ok(download_url) => {
info!(
"Successfully generated download signature URL: object_key={}",
object_key
);
let response = DownloadSignUrlResponse {
download_url,
oss_file_name: object_key,
oss_bucket: oss_client.get_config().bucket.clone(),
expires_in_hours: 4,
};
ApiResponse::success(response).into_response()
}
Err(e) => {
error!(
"Failed to generate download signature URL: file_name={}, error={}",
params.file_name, e
);
ApiResponse::internal_error::<DownloadSignUrlResponse>(&format!(
"生成下载签名URL失败: {e}"
))
.into_response()
}
}
}
/// 删除OSS文件
#[utoipa::path(
get,
path = "/api/v1/oss/delete",
params(
("file_name" = String, Query, description = "要删除的文件名"),
("bucket" = Option<String>, Query, description = "存储桶名称(可选)")
),
responses(
(status = 200, description = "删除成功", body = DeleteFileResponse),
(status = 400, description = "请求参数错误(如文件名为空)"),
(status = 404, description = "文件不存在"),
(status = 500, description = "服务器内部错误如OSS客户端未配置")
),
tag = "oss"
)]
pub async fn delete_file_from_oss(
State(state): State<AppState>,
Query(params): Query<DeleteFileParams>,
) -> impl IntoResponse {
info!(
"Delete OSS file request: file_name={}, bucket={:?}",
params.file_name, params.bucket
);
// 验证文件名
if params.file_name.trim().is_empty() {
return ApiResponse::validation_error::<DeleteFileResponse>("文件名不能为空")
.into_response();
}
// 检查OSS客户端是否可用
let oss_client = match &state.private_oss_client {
Some(client) => client,
None => {
error!("The OSS client is not configured");
return ApiResponse::internal_error::<DeleteFileResponse>("OSS客户端未配置")
.into_response();
}
};
// 构建完整的对象键名如果没有前缀则添加edu前缀
let object_key = if params.file_name.starts_with("edu/") {
// 如果已经包含前缀,直接使用
params.file_name.clone()
} else {
// 添加edu前缀
format!("edu/{}", params.file_name.trim_start_matches('/'))
};
// 先检查文件是否存在
match oss_client.file_exists(&object_key).await {
Ok(exists) => {
if !exists {
warn!("The file to be deleted does not exist: {}", object_key);
return ApiResponse::not_found::<DeleteFileResponse>("文件不存在").into_response();
}
}
Err(e) => {
error!(
"Failed to check file existence: file_name={}, error={}",
params.file_name, e
);
return ApiResponse::internal_error::<DeleteFileResponse>(&format!(
"检查文件存在性失败: {e}"
))
.into_response();
}
}
// 删除文件
match oss_client.delete_file(&object_key).await {
Ok(_) => {
info!("OSS file deleted successfully: object_key={}", object_key);
let response = DeleteFileResponse {
oss_file_name: object_key,
oss_bucket: oss_client.get_config().bucket.clone(),
message: "文件删除成功".to_string(),
};
ApiResponse::success(response).into_response()
}
Err(e) => {
error!(
"Failed to delete OSS file: file_name={}, error={}",
params.file_name, e
);
ApiResponse::internal_error::<DeleteFileResponse>(&format!("删除文件失败: {e}"))
.into_response()
}
}
}