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:
parent
5612f16425
commit
45d70e1dc6
@ -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;
|
||||
|
||||
@ -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, "图片文件损坏或格式不正确");
|
||||
}
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user