Skip to main content

yggdrasil/tasks/
orphan_asset_purge.rs

1//! 孤儿素材定期清理后台任务。
2//!
3//! 仅在 `server` feature 启用时编译,每天运行一次。
4//! 每次执行前读取 settings 表:若自动清理关闭则跳过,否则物理删除
5//! 「无文章、笔记版本、评论(含回收站评论)或头像引用,且超过保留天数」的素材
6//! (文件 + DB 行 + 派生缓存),语义与 /admin/assets 的「一键清理孤儿」
7//! 一致,只是改为定时自动。
8//!
9//! 评论区允许匿名传图后,未提交/被彻底删除评论留下的孤儿图会持续累积,
10//! 此任务是 uploads/ 磁盘的主要回收手段。
11
12use std::time::Duration;
13
14use tokio::time::interval;
15
16use crate::api::assets::delete::purge_orphan_assets_impl;
17use crate::api::assets::types::PurgeOrphansResponse;
18use crate::api::error::{ApiError, AppError};
19use crate::db::pool::get_conn;
20use crate::models::settings::AssetPurgeSettings;
21
22/// 启动孤儿素材清理循环,每天触发一次。
23///
24/// 每次读取最新配置:若 `asset_orphan_purge_enabled` 关闭则 no-op,
25/// 否则删除创建时间早于 `asset_orphan_retention_days` 的孤儿素材。
26/// 任何错误只记录日志,不中断循环。
27pub async fn run_purge() {
28    let mut ticker = interval(Duration::from_secs(86400));
29    loop {
30        match purge_orphans().await {
31            Ok(Some(res)) if res.deleted_count > 0 || res.failures > 0 => tracing::info!(
32                "Orphan asset purge: removed {} assets, freed {} bytes, {} file failures",
33                res.deleted_count,
34                res.freed_bytes,
35                res.failures
36            ),
37            Ok(_) => {}
38            Err(e) => tracing::error!("Orphan asset purge error: {}", e),
39        }
40        ticker.tick().await;
41    }
42}
43
44/// 读取配置并删除过期孤儿素材;自动清理关闭时返回 `None`。
45///
46/// 删除复用管理端 [`purge_orphan_assets_impl`] 的流程:事务内锁候选行、
47/// 删除语句复核引用、提交后再删文件,避免并发保存刚建立的引用被级联删除。
48///
49/// [`purge_orphan_assets_impl`]: crate::api::assets::delete::purge_orphan_assets_impl
50async fn purge_orphans() -> Result<Option<PurgeOrphansResponse>, ApiError> {
51    let client = get_conn().await.map_err(AppError::db_conn)?;
52    let setting = |key: &'static str| {
53        let client = &client;
54        async move {
55            client
56                .query_opt("SELECT value FROM settings WHERE key = $1", &[&key])
57                .await
58                .map(|row| row.map(|r| r.get::<_, String>("value")))
59                .map_err(AppError::query)
60        }
61    };
62
63    // 缺键时回退默认值(默认启用、7 天)。
64    let enabled = setting("asset_orphan_purge_enabled")
65        .await?
66        .and_then(|v| v.parse().ok())
67        .unwrap_or(crate::models::settings::DEFAULT_ASSET_ORPHAN_PURGE_ENABLED);
68    if !enabled {
69        return Ok(None);
70    }
71    let days = setting("asset_orphan_retention_days")
72        .await?
73        .and_then(|v| v.parse().ok())
74        .unwrap_or(crate::models::settings::DEFAULT_ASSET_ORPHAN_RETENTION_DAYS);
75    // 先归还读配置的连接,清理流程自行取连接开事务。
76    drop(client);
77
78    purge_orphan_assets_impl(AssetPurgeSettings::clamp_retention(days))
79        .await
80        .map(Some)
81}