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, pub original_filename: Option, } /// 下载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, } /// 获取上传签名URL请求参数 #[derive(Debug, Deserialize, ToSchema)] pub struct GetUploadSignUrlParams { pub file_name: String, pub content_type: Option, pub bucket: Option, } /// 获取下载签名URL请求参数(4小时有效) #[derive(Debug, Deserialize, ToSchema)] pub struct GetDownloadSignUrlParams { pub file_name: String, pub bucket: Option, } /// 删除文件请求参数 #[derive(Debug, Deserialize, ToSchema)] pub struct DeleteFileParams { pub file_name: String, pub bucket: Option, } /// 上传签名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, 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::("OSS客户端未配置") .into_response(); } }; let mut file_path: Option = None; let mut original_filename: Option = 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::("文件数据读取失败") .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::("临时文件创建失败") .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::("文件写入失败") .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::("未提供文件") .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); // 生成下载URL(4小时有效) 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::(&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, Query, description = "文件内容类型,默认为 application/octet-stream"), ("bucket" = Option, 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, Query(params): Query, ) -> 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::("文件名不能为空") .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::("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::(&format!( "生成上传签名URL失败: {e}" )) .into_response() } } } /// 获取下载签名URL(4小时有效) #[utoipa::path( get, path = "/api/v1/oss/download-sign-url", params( ("file_name" = String, Query, description = "文件名"), ("bucket" = Option, 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, Query(params): Query, ) -> 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::("文件名不能为空") .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::("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::(&format!( "生成下载签名URL失败: {e}" )) .into_response() } } } /// 删除OSS文件 #[utoipa::path( get, path = "/api/v1/oss/delete", params( ("file_name" = String, Query, description = "要删除的文件名"), ("bucket" = Option, 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, Query(params): Query, ) -> 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::("文件名不能为空") .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::("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::("文件不存在").into_response(); } } Err(e) => { error!( "Failed to check file existence: file_name={}, error={}", params.file_name, e ); return ApiResponse::internal_error::(&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::(&format!("删除文件失败: {e}")) .into_response() } } }