refactor(assets): 清理 lint,对齐项目惯例

- api/assets/mod.rs 按 posts/comments 惯例加
  #![allow(clippy::unused_unit, deprecated, unused_imports)]
  (#[server] 宏固有噪音)
- 页面交互导入按 target_arch = wasm32 门控;组件级放行非 WASM 未用绑定
- 合并 check_upload_dimensions 进 upload_dimensions(单函数返回尺寸,
  消除死代码),测试同步更名
This commit is contained in:
xfy 2026-07-24 16:44:47 +08:00
parent 5612f16425
commit 45d70e1dc6
4 changed files with 43 additions and 40 deletions

View File

@ -3,6 +3,8 @@
//! 管理 `uploads/` 图片的注册表assets 表与引用关系asset_refs 表):
//! 分页列表、删除保护、孤儿清理、全量重建索引。
//! 全部为 Dioxus server function仅 admin 可用。
// 与 posts / comments 模块一致Dioxus `#[server]` 宏触发 deprecated/unit 提示,按项目惯例放行。
#![allow(clippy::unused_unit, deprecated, unused_imports)]
/// 素材删除与孤儿清理。
pub mod delete;

View File

@ -321,22 +321,14 @@ fn check_image_dimensions(width: u32, height: u32) -> Result<(), StatusCode> {
}
#[cfg(feature = "server")]
/// 仅读取 header 校验上传图片的尺寸/像素是否超限
/// 仅读取 header 校验上传图片的尺寸/像素是否超限,并返回 (width, height)
///
/// 与 `check_image_dimensions` 的区别:
/// - 输入是原始字节 + MIME,内部按 MIME 分发只解析 header 拿尺寸(不解码像素)
/// - 返回带友好中文提示的 `&'static str`(供上传接口直接回给用户)
///
/// 上传入口三种格式在此统一拦截至尺寸上限;WebP 走 `zenwebp` header,
/// JPEG/PNG/GIF 走 `image` crate 的 `into_dimensions`(均只读 header)。
pub fn check_upload_dimensions(data: &[u8], mime_type: &str) -> Result<(), &'static str> {
upload_dimensions(data, mime_type).map(|_| ())
}
#[cfg(feature = "server")]
/// 与 [`check_upload_dimensions`] 相同的校验,但返回 (width, height)。
///
/// 供 `upload_image` 在校验通过后直接把尺寸写入 assets 表,避免二次解析 header。
/// 尺寸随结果返回,供 `upload_image` 校验通过后直接写入 assets 表,避免二次解析。
pub(crate) fn upload_dimensions(data: &[u8], mime_type: &str) -> Result<(u32, u32), &'static str> {
let dims = read_dimensions_by_mime(data, mime_type)?;
let (width, height) = dims;
@ -796,7 +788,7 @@ mod tests {
assert_eq!(dims, None);
}
// —— check_upload_dimensions:统一上传尺寸/像素上限校验 ——
// —— upload_dimensions统一上传尺寸/像素上限校验 ——
/// 构造指定尺寸的 PNG 字节(内存占用 = 尺寸,仅用于 header 校验测试)。
fn make_png_bytes(w: u32, h: u32) -> Vec<u8> {
@ -807,54 +799,54 @@ mod tests {
}
#[test]
fn check_upload_dimensions_accepts_small_png() {
fn upload_dimensions_accepts_small_png() {
let data = make_png_bytes(100, 100);
assert!(check_upload_dimensions(&data, "image/png").is_ok());
assert!(upload_dimensions(&data, "image/png").is_ok());
}
#[test]
fn check_upload_dimensions_accepts_boundary_png() {
fn upload_dimensions_accepts_boundary_png() {
// 用 7000×7000(≈49M 像素):单边 7000 < 默认 8192 上限,且总像素 < 默认 50M 上限,应放行。
// 注意不能直接用 *MAX_IMAGE_DIMENSION 做正方形边——8192²≈67M 会触发像素上限拒绝。
let data = make_png_bytes(7000, 7000);
assert!(check_upload_dimensions(&data, "image/png").is_ok());
assert!(upload_dimensions(&data, "image/png").is_ok());
}
#[test]
fn check_upload_dimensions_rejects_oversized_width() {
fn upload_dimensions_rejects_oversized_width() {
// 单边超限:(上限+1)×1,像素远低于上限,但单边越界
let data = make_png_bytes(*MAX_IMAGE_DIMENSION + 1, 1);
let err = check_upload_dimensions(&data, "image/png").unwrap_err();
let err = upload_dimensions(&data, "image/png").unwrap_err();
assert!(err.contains("尺寸过大"));
}
#[test]
fn check_upload_dimensions_rejects_oversized_height() {
fn upload_dimensions_rejects_oversized_height() {
let data = make_png_bytes(1, *MAX_IMAGE_DIMENSION + 1);
let err = check_upload_dimensions(&data, "image/png").unwrap_err();
let err = upload_dimensions(&data, "image/png").unwrap_err();
assert!(err.contains("尺寸过大"));
}
#[test]
fn check_upload_dimensions_accepts_small_webp() {
fn upload_dimensions_accepts_small_webp() {
let img = image::DynamicImage::new_rgb8(64, 48);
let webp_bytes = crate::webp::encode(&img, 85.0, 2).unwrap();
assert!(check_upload_dimensions(&webp_bytes, "image/webp").is_ok());
assert!(upload_dimensions(&webp_bytes, "image/webp").is_ok());
}
#[test]
fn check_upload_dimensions_accepts_gif() {
fn upload_dimensions_accepts_gif() {
// image crate 默认启用 gif feature,into_dimensions 可读 GIF header
let img = image::DynamicImage::new_rgb8(32, 32);
let mut buf = std::io::Cursor::new(Vec::new());
img.write_to(&mut buf, image::ImageFormat::Gif).unwrap();
assert!(check_upload_dimensions(&buf.into_inner(), "image/gif").is_ok());
assert!(upload_dimensions(&buf.into_inner(), "image/gif").is_ok());
}
#[test]
fn check_upload_dimensions_rejects_corrupt_bytes() {
fn upload_dimensions_rejects_corrupt_bytes() {
// 非 magic bytes,与现有损坏文件校验文案对齐
let err = check_upload_dimensions(b"not an image at all", "image/png").unwrap_err();
let err = upload_dimensions(b"not an image at all", "image/png").unwrap_err();
assert_eq!(err, "图片文件损坏或格式不正确");
}

View File

@ -18,11 +18,13 @@ use crate::models::asset::{AssetFilter, AssetSort};
/// - `on_select`:选中回填,参数为 `/uploads/<path>` URL。
/// - `cover_uploading`modal 内上传新图时置位,供父页面拦截保存(与 CoverUploader 语义一致)。
#[component]
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))]
pub fn AssetPickerModal(
mut visible: Signal<bool>,
on_select: EventHandler<String>,
cover_uploading: Signal<bool>,
) -> Element {
#[allow(unused_mut)]
let mut assets: Signal<Vec<AssetDto>> = use_signal(Vec::new);
#[allow(unused_mut)]
let mut loading = use_signal(|| false);
@ -34,21 +36,20 @@ pub fn AssetPickerModal(
use_effect(move || {
let open = visible();
let q = query();
if !open {
return;
}
#[cfg(target_arch = "wasm32")]
spawn(async move {
loading.set(true);
match list_assets(AssetFilter::All, q, AssetSort::CreatedDesc, 1).await {
Ok(resp) => {
assets.set(resp.assets);
error.set(None);
if open {
#[cfg(target_arch = "wasm32")]
spawn(async move {
loading.set(true);
match list_assets(AssetFilter::All, q, AssetSort::CreatedDesc, 1).await {
Ok(resp) => {
assets.set(resp.assets);
error.set(None);
}
Err(e) => error.set(Some(e.to_string())),
}
Err(e) => error.set(Some(e.to_string())),
}
loading.set(false);
});
loading.set(false);
});
}
});
if !visible() {

View File

@ -6,12 +6,17 @@
use dioxus::prelude::*;
// server fn 仅在 WASM 前端调用(全部包在 cfg(wasm32) 块内server SSR 只编译类型。
#[cfg(target_arch = "wasm32")]
use crate::api::assets::{
delete_asset, list_assets, purge_orphan_assets, rebuild_assets_index, update_asset_alt,
};
use crate::api::assets::{AssetListResponse, PurgeOrphansResponse, RebuildAssetsResponse};
use crate::api::assets::AssetListResponse;
#[cfg(target_arch = "wasm32")]
use crate::api::assets::{PurgeOrphansResponse, RebuildAssetsResponse};
use crate::components::empty_state::EmptyState;
use crate::components::ui::{FilterTabs, Pagination};
#[cfg(target_arch = "wasm32")]
use crate::models::asset::{AssetFilter, AssetSort};
/// 每页素材数,与服务端 list.rs 的 PER_PAGE 对齐。
@ -35,6 +40,8 @@ fn format_bytes(bytes: i64) -> String {
}
/// 素材管理入口组件。
// 交互逻辑全部 cfg(wasm32) 门控server SSR 编译时一批绑定未使用,按 CoverUploader 惯例放行。
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))]
#[component]
pub fn Assets() -> Element {
// 筛选/搜索/排序/分页状态:全部客户端驱动(单路由 + signal对齐「管理文章」模式
@ -43,6 +50,7 @@ pub fn Assets() -> Element {
let mut sort = use_signal(|| "created".to_string());
let mut page = use_signal(|| 1_i32);
#[allow(unused_mut)]
let mut data: Signal<Option<AssetListResponse>> = use_signal(|| None);
#[allow(unused_mut)]
let mut loading: Signal<bool> = use_signal(|| true);