yggdrasil/tasks/
orphan_asset_purge.rs1use std::time::Duration;
13
14use tokio::time::interval;
15
16use crate::db::pool::get_conn;
17use crate::models::settings::AssetPurgeSettings;
18
19pub async fn run_purge() {
25 let mut ticker = interval(Duration::from_secs(86400));
26 loop {
27 match get_conn().await {
28 Ok(client) => match purge_orphans(&client).await {
29 Ok((n, bytes)) => {
30 if n > 0 {
31 tracing::info!(
32 "Orphan asset purge: removed {} assets, freed {} bytes",
33 n,
34 bytes
35 );
36 }
37 }
38 Err(e) => tracing::error!("Orphan asset purge error: {:?}", e),
39 },
40 Err(e) => tracing::error!(
41 "Failed to get DB connection for orphan asset purge: {:?}",
42 e
43 ),
44 }
45 ticker.tick().await;
46 }
47}
48
49async fn purge_orphans(
54 client: &tokio_postgres::Client,
55) -> Result<(u64, i64), tokio_postgres::Error> {
56 let enabled: bool = client
58 .query_opt(
59 "SELECT value FROM settings WHERE key = 'asset_orphan_purge_enabled'",
60 &[],
61 )
62 .await?
63 .and_then(|r| r.get::<_, String>("value").parse().ok())
64 .unwrap_or(crate::models::settings::DEFAULT_ASSET_ORPHAN_PURGE_ENABLED);
65
66 if !enabled {
67 return Ok((0, 0));
68 }
69
70 let days: i32 = client
71 .query_opt(
72 "SELECT value FROM settings WHERE key = 'asset_orphan_retention_days'",
73 &[],
74 )
75 .await?
76 .and_then(|r| r.get::<_, String>("value").parse().ok())
77 .unwrap_or(crate::models::settings::DEFAULT_ASSET_ORPHAN_RETENTION_DAYS);
78
79 let days = AssetPurgeSettings::clamp_retention(days);
80
81 let rows = client
83 .query(
84 &format!(
85 "SELECT a.id AS id, a.path, a.size_bytes FROM assets a \
86 WHERE NOT EXISTS (SELECT 1 FROM asset_refs r WHERE r.asset_id = a.id) \
87 AND NOT {comment_ref} \
88 AND a.created_at < NOW() - make_interval(days => $1)",
89 comment_ref = crate::api::assets::COMMENT_REF_CLAUSE
90 ),
91 &[&days],
92 )
93 .await?;
94
95 if rows.is_empty() {
96 return Ok((0, 0));
97 }
98
99 let mut ids: Vec<uuid::Uuid> = Vec::with_capacity(rows.len());
100 let mut freed_bytes: i64 = 0;
101 for row in &rows {
102 let id: uuid::Uuid = row.get("id");
103 let path: String = row.get("path");
104 freed_bytes += row.get::<_, i64>("size_bytes");
105 let file_path = format!("uploads/{}", path);
106 if let Err(e) = tokio::fs::remove_file(&file_path).await {
107 if e.kind() != std::io::ErrorKind::NotFound {
108 tracing::warn!("Orphan purge: remove file failed ({}): {}", file_path, e);
109 }
110 }
111 crate::api::image::invalidate_asset_caches(&path).await;
112 ids.push(id);
113 }
114
115 let n = client
116 .execute("DELETE FROM assets WHERE id = ANY($1)", &[&ids])
117 .await?;
118 Ok((n, freed_bytes))
119}