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 表):
|
//! 管理 `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;
|
||||||
|
|||||||
@ -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, "图片文件损坏或格式不正确");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user