use chrono::{DateTime, Duration, SecondsFormat, Utc};
use laterite_core::strata::*;
use laterite_core::Db;
use std::collections::HashMap;
use std::sync::Arc;
use crate::ingest::blob_path;
use crate::link::MediaLinks;
use crate::record::{self, from_row, Media, MediaRecord, RecordError, COLUMNS};
use crate::StorageDriver;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct GcReport {
pub records: usize,
pub blobs: usize,
pub kept: Vec<String>,
}
pub async fn garbage(db: &Db, before: DateTime<Utc>) -> Result<Vec<MediaRecord>, RecordError> {
let cutoff = before.to_rfc3339_opts(SecondsFormat::Micros, true);
let (sql, values) = build(
db.backend,
Query::select()
.columns(COLUMNS)
.from(Media::Table)
.and_where(Expr::col(Media::InLibrary).eq(0))
.and_where(Expr::col(Media::CreatedAt).lt(cutoff))
.and_where(
Expr::col(Media::Id).not_in_subquery(
Query::select()
.column(MediaLinks::MediaId)
.from(MediaLinks::Table)
.to_owned(),
),
)
.order_by(Media::Id, Order::Asc)
.to_owned(),
);
let rows = bind_values(sqlx::query(&sql), values)
.fetch_all(&db.pool)
.await?;
Ok(rows.iter().map(from_row).collect::<Result<_, _>>()?)
}
pub async fn collect(
db: &Db,
disks: &HashMap<String, Arc<dyn StorageDriver>>,
grace: Duration,
) -> Result<GcReport, RecordError> {
let before = Utc::now() - grace;
let mut report = GcReport::default();
for r in garbage(db, before).await? {
let variants = crate::variant::list_for(db, r.id).await?;
crate::variant::delete_for(db, r.id).await?;
let last = record::delete(db, r.id).await?;
report.records += 1;
if !last {
continue;
}
let removed = match disks.get(&r.disk) {
Some(driver) => {
for v in &variants {
let _ = driver.delete(&v.path).await;
}
driver.delete(&blob_path(&r.hash)).await.is_ok()
}
None => false,
};
if removed {
report.blobs += 1;
} else {
report.kept.push(format!("{}:{}", r.disk, r.hash));
}
}
Ok(report)
}