juiceback/jobs/
cleanup.rs1use std::sync::Arc;
4use std::time::Duration;
5
6use crate::db;
7use crate::error::AppError;
8use crate::state::AppState;
9use chrono::Utc;
10
11pub async fn run_cleanup_loop(state: Arc<AppState>) {
13 let interval = Duration::from_secs(state.config.cleanup_interval_minutes * 60);
14 let mut ticker = tokio::time::interval(interval);
15 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
16
17 loop {
18 ticker.tick().await;
19 run_cleanup_once(&state).await;
20 }
21}
22
23async fn run_cleanup_once(state: &Arc<AppState>) {
24 tracing::info!("cleanup: starting");
25
26 let state2 = Arc::clone(state);
27 let expired = match tokio::task::spawn_blocking(move || -> Result<Vec<db::FileRecord>, AppError> {
28 let db = state2.db.get().map_err(|e| AppError::DbPoolError(e.to_string()))?;
29 db::list_expired(&db).map_err(AppError::DatabaseError)
30 })
31 .await
32 {
33 Ok(Ok(records)) => records,
34 Ok(Err(e)) => {
35 tracing::error!("cleanup: db query failed: {:?}", e);
36 return;
37 }
38 Err(e) => {
39 tracing::error!("cleanup: spawn_blocking panicked: {}", e);
40 return;
41 }
42 };
43
44 if expired.is_empty() {
45 tracing::info!("cleanup: nothing to clean");
46 return;
47 }
48
49 tracing::info!("cleanup: found {} expired files", expired.len());
50
51 let ids_to_purge: Vec<String> = expired.iter().map(|r| r.id.clone()).collect();
52
53 let state3 = Arc::clone(state);
55 let ids_for_purge = ids_to_purge.clone();
56 let purge_result = tokio::task::spawn_blocking(move || -> Result<usize, AppError> {
57 let db = state3.db.get().map_err(|e| AppError::DbPoolError(e.to_string()))?;
58 db::delete_files_by_ids(&db, &ids_for_purge).map_err(AppError::DatabaseError)
59 })
60 .await
61 .map_err(|e| tracing::error!("cleanup: purge task panicked: {}", e));
62
63 match purge_result {
64 Ok(Ok(count)) => tracing::info!("cleanup: purged {} rows from db", count),
65 Ok(Err(e)) => tracing::error!("cleanup: failed to purge db rows: {:?}", e),
66 Err(_) => {}
67 }
68
69 for record in &expired {
71 let host_opt = if record.storage_host.is_empty() { None } else { Some(record.storage_host.as_str()) };
72 crate::juicehost::delete_file_on_juicehost(state, &record.id, host_opt).await;
73 }
74
75 let now = Utc::now().timestamp();
77 let stale_ids: Vec<String> = state
78 .tus
79 .iter()
80 .filter(|e| now - e.created_at > 3600)
81 .map(|e| e.id.clone())
82 .collect();
83 for id in &stale_ids {
84 if let Some((_, upload)) = state.tus.remove(id) {
85 tracing::info!("tus cleanup: removing abandoned upload id={}", id);
86 let _ = tokio::fs::remove_file(&upload.storage_path).await;
87 }
88 }
89 if !stale_ids.is_empty() {
90 tracing::info!("tus cleanup: removed {} abandoned uploads", stale_ids.len());
91 }
92}