- 新增 upload.rs 模块,支持 multipart/form-data 文件上传 - 文件按日期存储在 storage/YYYY-MM-DD/ 目录下 - 使用 UUID 生成唯一文件名,保留原始扩展名 - 添加 axum-extra, uuid, chrono 依赖 新增测试用例: - config_test.rs: 6 个测试 (配置结构验证) - router_test.rs: 11 个测试 (路由匹配逻辑) - handler_test.rs: 8 个测试 (请求处理) - upload_test.rs: 13 个测试 (文件上传功能) 其他改进: - 优化 handler.rs 代码注释 - 更新 .gitignore 忽略 storage/ 和 .claude/ - 添加 CLAUDE.md 项目指南文档 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
151 lines
4.1 KiB
Rust
151 lines
4.1 KiB
Rust
use axum::{
|
|
body::Body,
|
|
extract::State,
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
};
|
|
use axum_extra::extract::Multipart;
|
|
use chrono::Local;
|
|
use serde_json::json;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use tokio::fs;
|
|
use uuid::Uuid;
|
|
|
|
use crate::handler::AppState;
|
|
|
|
/// 文件上传结果
|
|
#[derive(Debug, serde::Serialize)]
|
|
pub struct UploadResult {
|
|
pub success: bool,
|
|
pub files: Vec<FileInfo>,
|
|
pub message: String,
|
|
}
|
|
|
|
/// 单个文件信息
|
|
#[derive(Debug, serde::Serialize)]
|
|
pub struct FileInfo {
|
|
pub original_name: String,
|
|
pub stored_name: String,
|
|
pub path: String,
|
|
pub size: usize,
|
|
pub content_type: Option<String>,
|
|
}
|
|
|
|
impl UploadResult {
|
|
pub fn success(files: Vec<FileInfo>) -> Self {
|
|
UploadResult {
|
|
success: true,
|
|
files,
|
|
message: "Files uploaded successfully".to_string(),
|
|
}
|
|
}
|
|
|
|
pub fn error(message: String) -> Self {
|
|
UploadResult {
|
|
success: false,
|
|
files: vec![],
|
|
message,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl IntoResponse for UploadResult {
|
|
fn into_response(self) -> Response {
|
|
let status = if self.success {
|
|
StatusCode::OK
|
|
} else {
|
|
StatusCode::BAD_REQUEST
|
|
};
|
|
|
|
let body = serde_json::to_string(&self).unwrap_or_else(|_| {
|
|
json!({
|
|
"success": false,
|
|
"files": [],
|
|
"message": "Failed to serialize response"
|
|
})
|
|
.to_string()
|
|
});
|
|
|
|
Response::builder()
|
|
.status(status)
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(body))
|
|
.unwrap()
|
|
}
|
|
}
|
|
|
|
/// 处理文件上传
|
|
pub async fn upload_handler(
|
|
State(_state): State<Arc<AppState>>,
|
|
mut multipart: Multipart,
|
|
) -> impl IntoResponse {
|
|
let mut uploaded_files = Vec::new();
|
|
|
|
// 确保 storage 目录存在
|
|
let storage_root = PathBuf::from("storage");
|
|
if let Err(e) = fs::create_dir_all(&storage_root).await {
|
|
return UploadResult::error(format!("Failed to create storage directory: {}", e));
|
|
}
|
|
|
|
// 创建按日期分类的子目录
|
|
let date_dir = Local::now().format("%Y-%m-%d").to_string();
|
|
let upload_dir = storage_root.join(&date_dir);
|
|
if let Err(e) = fs::create_dir_all(&upload_dir).await {
|
|
return UploadResult::error(format!("Failed to create upload directory: {}", e));
|
|
}
|
|
|
|
// 处理每个上传的字段
|
|
while let Some(field) = multipart.next_field().await.unwrap_or(None) {
|
|
// 获取文件名
|
|
let original_name = field
|
|
.file_name()
|
|
.map(|s| s.to_string())
|
|
.unwrap_or_else(|| "unknown".to_string());
|
|
|
|
// 获取内容类型
|
|
let content_type = field.content_type().map(|s| s.to_string());
|
|
|
|
// 读取文件数据
|
|
let data = match field.bytes().await {
|
|
Ok(d) => d,
|
|
Err(e) => {
|
|
eprintln!("Failed to read file data: {}", e);
|
|
continue;
|
|
}
|
|
};
|
|
|
|
// 生成唯一文件名,保留原始扩展名
|
|
let extension = std::path::Path::new(&original_name)
|
|
.extension()
|
|
.and_then(|s| s.to_str())
|
|
.map(|s| format!(".{}", s))
|
|
.unwrap_or_default();
|
|
|
|
let stored_name = format!("{}{}", Uuid::new_v4(), extension);
|
|
let file_path = upload_dir.join(&stored_name);
|
|
|
|
// 保存文件
|
|
match fs::write(&file_path, &data).await {
|
|
Ok(_) => {
|
|
uploaded_files.push(FileInfo {
|
|
original_name: original_name.clone(),
|
|
stored_name: stored_name.clone(),
|
|
path: format!("storage/{}/{}", date_dir, stored_name),
|
|
size: data.len(),
|
|
content_type,
|
|
});
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Failed to save file {}: {}", original_name, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
if uploaded_files.is_empty() {
|
|
UploadResult::error("No files were uploaded".to_string())
|
|
} else {
|
|
UploadResult::success(uploaded_files)
|
|
}
|
|
}
|