Files
qiming/qiming-mcp-proxy/document-parser/src/handlers/private_oss_handler.rs
2026-06-01 13:03:20 +08:00

487 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
}
}
}