Skip to main content

yggdrasil/api/assets/
mod.rs

1//! 素材管理 API 模块。
2//!
3//! 管理 `uploads/` 图片的注册表(assets 表)与引用关系(asset_refs 表):
4//! 分页列表、删除保护、孤儿清理、全量重建索引。
5//! 全部为 Dioxus server function,仅 admin 可用。
6// 与 posts / comments 模块一致:Dioxus `#[server]` 宏触发 deprecated/unit 提示,按项目惯例放行。
7#![allow(clippy::unused_unit, deprecated, unused_imports)]
8
9/// 素材引用判定 SQL 片段(表别名固定为 `a`,即 assets)。
10///
11/// 引用来源包括文章关联(`asset_refs`)、存活评论 HTML、用户头像和友链头像。
12/// 文章关联由索引重建维护;后三者保存的是直接 URL,因此在查询时按素材路径判断。
13#[cfg(feature = "server")]
14pub(crate) const ASSET_REF_CLAUSE: &str =
15    "(EXISTS (SELECT 1 FROM asset_refs r WHERE r.asset_id = a.id) \
16     OR EXISTS (SELECT 1 FROM comments c \
17               WHERE c.deleted_at IS NULL AND c.content_html LIKE '%' || a.path || '%') \
18     OR EXISTS (SELECT 1 FROM users u WHERE u.avatar_url = '/uploads/' || a.path) \
19     OR EXISTS (SELECT 1 FROM friend_links f WHERE f.avatar_url = '/uploads/' || a.path))";
20
21/// 存活评论引用判定,供单删接口返回更具体的业务提示。
22#[cfg(feature = "server")]
23pub(crate) const COMMENT_REF_CLAUSE: &str = "EXISTS (SELECT 1 FROM comments c \
24     WHERE c.deleted_at IS NULL AND c.content_html LIKE '%' || a.path || '%')";
25
26/// 直接头像引用判定,供单删接口区分评论引用。
27#[cfg(feature = "server")]
28pub(crate) const AVATAR_REF_CLAUSE: &str =
29    "(EXISTS (SELECT 1 FROM users u WHERE u.avatar_url = '/uploads/' || a.path) \
30     OR EXISTS (SELECT 1 FROM friend_links f WHERE f.avatar_url = '/uploads/' || a.path))";
31
32/// 列表页引用计数表达式,按每个文章/评论/头像使用次数累计。
33#[cfg(feature = "server")]
34pub(crate) const ASSET_REF_COUNT_EXPR: &str =
35    "((SELECT COUNT(*) FROM asset_refs r WHERE r.asset_id = a.id) + \
36     (SELECT COUNT(*) FROM comments c WHERE c.deleted_at IS NULL \
37      AND c.content_html LIKE '%' || a.path || '%') + \
38     (SELECT COUNT(*) FROM users u WHERE u.avatar_url = '/uploads/' || a.path) + \
39     (SELECT COUNT(*) FROM friend_links f WHERE f.avatar_url = '/uploads/' || a.path))";
40
41/// 素材删除与孤儿清理。
42pub mod delete;
43/// 素材分页列表。
44pub mod list;
45/// 素材索引全量重建。
46pub mod rebuild;
47/// 请求与响应数据结构。
48pub mod types;
49
50pub use delete::{batch_delete_assets, delete_asset, purge_orphan_assets, update_asset_alt};
51pub use list::list_assets;
52pub use rebuild::rebuild_assets_index;
53pub use types::{
54    AssetListResponse, AssetOpResponse, BatchDeleteAssetsResponse, PurgeOrphansResponse,
55    RebuildAssetsResponse,
56};
57
58#[cfg(all(test, feature = "server"))]
59mod tests {
60    use super::ASSET_REF_CLAUSE;
61
62    #[test]
63    fn asset_reference_clause_protects_profile_avatar() {
64        assert!(
65            ASSET_REF_CLAUSE.contains("users u")
66                && ASSET_REF_CLAUSE.contains("u.avatar_url = '/uploads/' || a.path"),
67            "素材引用判定必须包含用户头像路径"
68        );
69    }
70}