refactor(upload): 抽取 upload_error() 消除 15 处 JSON 错误响应样板

upload.rs 中 14 处 return Err((StatusCode::X, Json(json!({"success":
false, "error": MSG})))) + 1 处 map_err 同款 tuple,全部替换为
upload_error(status, msg) 单行调用。helper 接受 impl Serialize,
兼容 &str / String / 字面量等调用形式。
This commit is contained in:
xfy 2026-07-14 14:53:18 +08:00
parent 8da678b3b4
commit 804bcfb7b7

View File

@ -24,6 +24,12 @@ const ALLOWED_MIME_TYPES: &[&str] = &["image/jpeg", "image/png", "image/gif", "i
#[cfg(feature = "server")] #[cfg(feature = "server")]
const MAX_FILE_SIZE: usize = 5 * 1024 * 1024; // 5MB const MAX_FILE_SIZE: usize = 5 * 1024 * 1024; // 5MB
/// 构造统一的 JSON 错误响应:`{ "success": false, "error": msg }`。
#[cfg(feature = "server")]
fn upload_error<T: serde::Serialize>(status: StatusCode, msg: T) -> (StatusCode, Json<Value>) {
(status, Json(json!({ "success": false, "error": msg })))
}
#[cfg(feature = "server")] #[cfg(feature = "server")]
fn mime_to_ext(mime: &str) -> &'static str { fn mime_to_ext(mime: &str) -> &'static str {
match mime { match mime {
@ -82,13 +88,7 @@ pub async fn upload_image(
let peer = connect_info.map(|Extension(ConnectInfo(addr))| addr); let peer = connect_info.map(|Extension(ConnectInfo(addr))| addr);
let ip = crate::api::rate_limit::get_client_ip_with_peer(&headers, peer); let ip = crate::api::rate_limit::get_client_ip_with_peer(&headers, peer);
if let Err(msg) = crate::api::rate_limit::check_upload_limit(&ip) { if let Err(msg) = crate::api::rate_limit::check_upload_limit(&ip) {
return Err(( return Err(upload_error(StatusCode::TOO_MANY_REQUESTS, msg));
StatusCode::TOO_MANY_REQUESTS,
Json(json!({
"success": false,
"error": msg
})),
));
} }
// 1. Extract session from cookie // 1. Extract session from cookie
@ -100,13 +100,7 @@ pub async fn upload_image(
let token = match parse_session_token(cookie_header) { let token = match parse_session_token(cookie_header) {
Some(t) => t, Some(t) => t,
None => { None => {
return Err(( return Err(upload_error(StatusCode::UNAUTHORIZED, "未登录"));
StatusCode::UNAUTHORIZED,
Json(json!({
"success": false,
"error": "未登录"
})),
));
} }
}; };
@ -114,60 +108,30 @@ pub async fn upload_image(
let user = match crate::api::auth::get_user_by_token(token).await { let user = match crate::api::auth::get_user_by_token(token).await {
Ok(Some(u)) => u, Ok(Some(u)) => u,
_ => { _ => {
return Err(( return Err(upload_error(StatusCode::UNAUTHORIZED, "会话已过期"));
StatusCode::UNAUTHORIZED,
Json(json!({
"success": false,
"error": "会话已过期"
})),
));
} }
}; };
if user.role != crate::models::user::UserRole::Admin { if user.role != crate::models::user::UserRole::Admin {
return Err(( return Err(upload_error(StatusCode::FORBIDDEN, "权限不足"));
StatusCode::FORBIDDEN,
Json(json!({
"success": false,
"error": "权限不足"
})),
));
} }
// 3. Read multipart field // 3. Read multipart field
let field = match multipart.next_field().await { let field = match multipart.next_field().await {
Ok(Some(f)) => f, Ok(Some(f)) => f,
Ok(None) => { Ok(None) => {
return Err(( return Err(upload_error(StatusCode::BAD_REQUEST, "未找到文件"));
StatusCode::BAD_REQUEST,
Json(json!({
"success": false,
"error": "未找到文件"
})),
));
} }
Err(e) => { Err(e) => {
tracing::error!("Multipart error: {:?}", e); tracing::error!("Multipart error: {:?}", e);
return Err(( return Err(upload_error(StatusCode::BAD_REQUEST, "文件读取失败"));
StatusCode::BAD_REQUEST,
Json(json!({
"success": false,
"error": "文件读取失败"
})),
));
} }
}; };
// 4. Validate mime type // 4. Validate mime type
let mime_type = field.content_type().unwrap_or("").to_string(); let mime_type = field.content_type().unwrap_or("").to_string();
if !ALLOWED_MIME_TYPES.contains(&mime_type.as_str()) { if !ALLOWED_MIME_TYPES.contains(&mime_type.as_str()) {
return Err(( return Err(upload_error(StatusCode::BAD_REQUEST, "不支持的文件类型"));
StatusCode::BAD_REQUEST,
Json(json!({
"success": false,
"error": "不支持的文件类型"
})),
));
} }
// 5. Read file data // 5. Read file data
@ -175,48 +139,24 @@ pub async fn upload_image(
Ok(d) => d, Ok(d) => d,
Err(e) => { Err(e) => {
tracing::error!("Read file error: {:?}", e); tracing::error!("Read file error: {:?}", e);
return Err(( return Err(upload_error(StatusCode::INTERNAL_SERVER_ERROR, "文件读取失败"));
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"success": false,
"error": "文件读取失败"
})),
));
} }
}; };
if data.len() > MAX_FILE_SIZE { if data.len() > MAX_FILE_SIZE {
return Err(( return Err(upload_error(StatusCode::PAYLOAD_TOO_LARGE, "文件超过大小限制"));
StatusCode::PAYLOAD_TOO_LARGE,
Json(json!({
"success": false,
"error": "文件超过大小限制"
})),
));
} }
// 校验文件头 magic bytes防止仅修改扩展名/Content-Type 上传非图片文件。 // 校验文件头 magic bytes防止仅修改扩展名/Content-Type 上传非图片文件。
if !validate_image_magic_bytes(&data, mime_type.as_str()) { if !validate_image_magic_bytes(&data, mime_type.as_str()) {
return Err(( return Err(upload_error(StatusCode::BAD_REQUEST, "文件类型与内容不符"));
StatusCode::BAD_REQUEST,
Json(json!({
"success": false,
"error": "文件类型与内容不符"
})),
));
} }
// 仅读 header 统一校验尺寸/像素上限。三种格式走同一路径: // 仅读 header 统一校验尺寸/像素上限。三种格式走同一路径:
// JPEG/PNG/GIF 用 image crate 的 into_dimensions,WebP 用 zenwebp header。 // JPEG/PNG/GIF 用 image crate 的 into_dimensions,WebP 用 zenwebp header。
// 超限直接拒绝,避免大图走 decode 后被静默降级(原 fallback 存原图)。 // 超限直接拒绝,避免大图走 decode 后被静默降级(原 fallback 存原图)。
if let Err(msg) = crate::api::image::check_upload_dimensions(&data, mime_type.as_str()) { if let Err(msg) = crate::api::image::check_upload_dimensions(&data, mime_type.as_str()) {
return Err(( return Err(upload_error(StatusCode::BAD_REQUEST, msg));
StatusCode::BAD_REQUEST,
Json(json!({
"success": false,
"error": msg
})),
));
} }
let is_gif = mime_type.as_str() == "image/gif"; let is_gif = mime_type.as_str() == "image/gif";
@ -231,23 +171,9 @@ pub async fn upload_image(
validate_raw_image(&validate_data, validate_mime.as_str()) validate_raw_image(&validate_data, validate_mime.as_str())
}) })
.await .await
.map_err(|_| { .map_err(|_| upload_error(StatusCode::INTERNAL_SERVER_ERROR, "图片校验任务失败"))?;
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"success": false,
"error": "图片校验任务失败"
})),
)
})?;
if !is_valid { if !is_valid {
return Err(( return Err(upload_error(StatusCode::BAD_REQUEST, "图片文件损坏或格式不正确"));
StatusCode::BAD_REQUEST,
Json(json!({
"success": false,
"error": "图片文件损坏或格式不正确"
})),
));
} }
} }
@ -342,24 +268,12 @@ pub async fn upload_image(
if let Err(e) = tokio::fs::create_dir_all(&dir_path).await { if let Err(e) = tokio::fs::create_dir_all(&dir_path).await {
tracing::error!("Create dir error: {:?}", e); tracing::error!("Create dir error: {:?}", e);
return Err(( return Err(upload_error(StatusCode::INTERNAL_SERVER_ERROR, "文件保存失败"));
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"success": false,
"error": "文件保存失败"
})),
));
} }
if let Err(e) = tokio::fs::write(&file_path, &final_data).await { if let Err(e) = tokio::fs::write(&file_path, &final_data).await {
tracing::error!("Write file error: {:?}", e); tracing::error!("Write file error: {:?}", e);
return Err(( return Err(upload_error(StatusCode::INTERNAL_SERVER_ERROR, "文件保存失败"));
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"success": false,
"error": "文件保存失败"
})),
));
} }
tracing::info!("Image uploaded: {} ({} bytes)", file_path, final_data.len()); tracing::info!("Image uploaded: {} ({} bytes)", file_path, final_data.len());