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

View File

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

View File

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

View File

@ -6,12 +6,17 @@
use dioxus::prelude::*; use dioxus::prelude::*;
// server fn 仅在 WASM 前端调用(全部包在 cfg(wasm32) 块内server SSR 只编译类型。
#[cfg(target_arch = "wasm32")]
use crate::api::assets::{ use crate::api::assets::{
delete_asset, list_assets, purge_orphan_assets, rebuild_assets_index, update_asset_alt, 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::empty_state::EmptyState;
use crate::components::ui::{FilterTabs, Pagination}; use crate::components::ui::{FilterTabs, Pagination};
#[cfg(target_arch = "wasm32")]
use crate::models::asset::{AssetFilter, AssetSort}; use crate::models::asset::{AssetFilter, AssetSort};
/// 每页素材数,与服务端 list.rs 的 PER_PAGE 对齐。 /// 每页素材数,与服务端 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] #[component]
pub fn Assets() -> Element { pub fn Assets() -> Element {
// 筛选/搜索/排序/分页状态:全部客户端驱动(单路由 + signal对齐「管理文章」模式 // 筛选/搜索/排序/分页状态:全部客户端驱动(单路由 + signal对齐「管理文章」模式
@ -43,6 +50,7 @@ pub fn Assets() -> Element {
let mut sort = use_signal(|| "created".to_string()); let mut sort = use_signal(|| "created".to_string());
let mut page = use_signal(|| 1_i32); let mut page = use_signal(|| 1_i32);
#[allow(unused_mut)]
let mut data: Signal<Option<AssetListResponse>> = use_signal(|| None); let mut data: Signal<Option<AssetListResponse>> = use_signal(|| None);
#[allow(unused_mut)] #[allow(unused_mut)]
let mut loading: Signal<bool> = use_signal(|| true); let mut loading: Signal<bool> = use_signal(|| true);