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 {asset_ref} \
87 AND a.created_at < NOW() - make_interval(days => $1)",
88 asset_ref = crate::api::assets::ASSET_REF_CLAUSE
89 ),
90 &[&days],
91 )
92 .await?;
93
94 if rows.is_empty() {
95 return Ok((0, 0));
96 }
97
98 let mut ids: Vec<uuid::Uuid> = Vec::with_capacity(rows.len());
99 let mut freed_bytes: i64 = 0;
100 for row in &rows {
101 let id: uuid::Uuid = row.get("id");
102 let path: String = row.get("path");
103 freed_bytes += row.get::<_, i64>("size_bytes");
104 let file_path = format!("uploads/{}", path);
105 if let Err(e) = tokio::fs::remove_file(&file_path).await {
106 if e.kind() != std::io::ErrorKind::NotFound {
107 tracing::warn!("Orphan purge: remove file failed ({}): {}", file_path, e);
108 }
109 }
110 crate::api::image::invalidate_asset_caches(&path).await;
111 ids.push(id);
112 }
113
114 let n = client
115 .execute("DELETE FROM assets WHERE id = ANY($1)", &[&ids])
116 .await?;
117 Ok((n, freed_bytes))
118}