From 604febde98b40d8678e2706fc3b7753d4fd4eb2b Mon Sep 17 00:00:00 2001 From: xfy Date: Wed, 15 Jul 2026 11:28:21 +0800 Subject: [PATCH] =?UTF-8?q?perf(upload):=20=E6=B6=88=E9=99=A4=20data.to=5F?= =?UTF-8?q?vec()=20=E5=A4=9A=E6=AC=A1=E5=85=A8=E6=96=87=E4=BB=B6=E6=B7=B1?= =?UTF-8?q?=E6=8B=B7=E8=B4=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit data 是引用计数的 Bytes,clone 廉价(原子 +1)。 - validate 路径:data.to_vec() → data.clone(),省一次全文件拷贝 - 转码闭包:original_data = data.to_vec() → data.clone(), 仅在「保留原格式」回退分支才 to_vec() 出落盘所需 Vec - 路径拼接:用 chrono DelayedFormat 内联,省 year/month/day 三个中间 String 5MB 图片上传:转码成功路径从 10MB 堆分配降到 0(仅 Bytes 引用计数); 回退路径从 10MB 降到 5MB。 --- src/api/upload.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/api/upload.rs b/src/api/upload.rs index 0d66bf2..fcf73b7 100644 --- a/src/api/upload.rs +++ b/src/api/upload.rs @@ -164,8 +164,9 @@ pub async fn upload_image( // 对不经过重编码的格式做解码验证。GIF 走 image::load_from_memory 会完整解码, // 移到阻塞线程池避免拖住 async 运行时。 + // data 是引用计数的 Bytes,clone 廉价(原子 +1),避免 to_vec() 的全文件深拷贝。 if is_gif || is_webp { - let validate_data = data.to_vec(); + let validate_data = data.clone(); let validate_mime = mime_type.clone(); let is_valid = tokio::task::spawn_blocking(move || { validate_raw_image(&validate_data, validate_mime.as_str()) @@ -178,12 +179,14 @@ pub async fn upload_image( } // GIF 与 WebP 保持原格式;其余格式尝试转 WebP。 - let (final_data, final_ext) = if is_gif { + // Bytes clone 廉价(引用计数 +1),move 进阻塞闭包无需全文件深拷贝; + // 仅在「保留原格式」回退分支才 to_vec() 出落盘所需的 Vec。 + let (final_data, final_ext): (Vec, String) = if is_gif { (data.to_vec(), "gif".to_string()) } else if is_webp { (data.to_vec(), "webp".to_string()) } else { - let original_data = data.to_vec(); + let original_data = data.clone(); let mime = mime_type.clone(); let config = crate::webp::WEBP_CONFIG.clone(); // 在阻塞线程中执行图片解码与 WebP 编码,避免阻塞异步运行时。 @@ -222,12 +225,12 @@ pub async fn upload_image( img.width(), img.height(), original_data.len(), webp_data.len() ); - (original_data, mime_to_ext(&mime).to_string(), false) + (original_data.to_vec(), mime_to_ext(&mime).to_string(), false) } } Err(e) => { tracing::warn!("WebP encode failed ({}), keeping original format", e); - (original_data, mime_to_ext(&mime).to_string(), false) + (original_data.to_vec(), mime_to_ext(&mime).to_string(), false) } }; result @@ -235,7 +238,7 @@ pub async fn upload_image( Err(e) => { // 到这里尺寸校验已通过(超限在 header 阶段被拒),decode 失败只能是真损坏。 tracing::warn!("Failed to decode image ({}), keeping original format", e); - (original_data, mime_to_ext(&mime).to_string(), false) + (original_data.to_vec(), mime_to_ext(&mime).to_string(), false) } } }) @@ -251,16 +254,15 @@ pub async fn upload_image( }; // 按上传时间组织目录:uploads/YYYY/MM/DD。 + // chrono 的 DelayedFormat 实现 Display,可直接进 format!,省掉 year/month/day 三个中间 String。 let now = chrono::Utc::now(); - let year = now.format("%Y").to_string(); - let month = now.format("%m").to_string(); - let day = now.format("%d").to_string(); + let date = now.format("%Y/%m/%d"); let uuid = uuid::Uuid::new_v4().to_string(); - let dir_path = format!("uploads/{}/{}/{}", year, month, day); + let dir_path = format!("uploads/{}", date); let file_name = format!("{}.{}.{}", now.format("%H%M%S"), uuid, final_ext); let file_path = format!("{}/{}", dir_path, file_name); - let url_path = format!("/uploads/{}/{}/{}/{}", year, month, day, file_name); + let url_path = format!("/uploads/{}/{}", date, file_name); if let Err(e) = tokio::fs::create_dir_all(&dir_path).await { tracing::error!("Create dir error: {:?}", e);