use crate::Result;
use crate::path_resolver::{parse_object_store_url, resolve_path};
use chrono::{DateTime, Utc};
use futures::TryStreamExt;
use object_store::path::Path as ObjectPath;
use object_store::{ObjectStore, ObjectStoreExt};
use std::collections::HashSet;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub enum ExpireCriteria {
Versions(Vec<i64>),
OlderThan(DateTime<Utc>),
}
#[derive(Debug, Clone)]
pub enum CleanupCriteria {
All,
OlderThan(DateTime<Utc>),
}
pub(crate) fn format_sql_timestamp(dt: &DateTime<Utc>) -> String {
dt.format("%Y-%m-%d %H:%M:%S%.6f").to_string()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExpiredSnapshot {
pub snapshot_id: i64,
pub snapshot_time: String,
}
#[derive(Debug, Clone)]
pub struct ScheduledFile {
pub data_file_id: i64,
pub path: String,
pub path_is_relative: bool,
}
async fn run_cleanup<RemoveFut>(
data_path: &str,
files: Vec<ScheduledFile>,
object_store: Arc<dyn ObjectStore>,
dry_run: bool,
remove_rows: impl FnOnce(Vec<i64>) -> RemoveFut,
) -> Result<Vec<String>>
where
RemoveFut: std::future::Future<Output = Result<()>>,
{
if files.is_empty() {
return Ok(Vec::new());
}
let (_, base_key) = parse_object_store_url(data_path)?;
let mut resolved = Vec::with_capacity(files.len());
let mut ids = Vec::with_capacity(files.len());
for file in &files {
let abs = resolve_path(&base_key, &file.path, file.path_is_relative)?;
resolved.push(abs);
ids.push(file.data_file_id);
}
if dry_run {
return Ok(resolved);
}
for abs in &resolved {
let key = ObjectPath::from(abs.trim_start_matches('/'));
match object_store.delete(&key).await {
Ok(()) => {},
Err(object_store::Error::NotFound {
..
}) => {},
Err(e) => return Err(e.into()),
}
}
remove_rows(ids).await?;
Ok(resolved)
}
#[cfg(feature = "write-sqlite")]
pub async fn cleanup_old_files_sqlite(
writer: &crate::metadata_writer_sqlite::SqliteMetadataWriter,
object_store: Arc<dyn ObjectStore>,
criteria: CleanupCriteria,
dry_run: bool,
) -> Result<Vec<String>> {
let data_path = crate::metadata_writer::MetadataWriter::get_data_path(writer)?;
let files = writer.list_scheduled_for_deletion(&criteria)?;
run_cleanup(&data_path, files, object_store, dry_run, |ids| async move {
writer.remove_scheduled(&ids)
})
.await
}
#[cfg(feature = "write-postgres")]
pub async fn cleanup_old_files_in_catalog(
mgr: &crate::multicatalog::MulticatalogManager,
catalog_name: &str,
object_store: Arc<dyn ObjectStore>,
criteria: CleanupCriteria,
dry_run: bool,
) -> Result<Vec<String>> {
let data_path = mgr.get_data_path().await?;
let files = mgr
.list_scheduled_for_deletion_in_catalog(catalog_name, &criteria)
.await?;
run_cleanup(&data_path, files, object_store, dry_run, |ids| async move {
mgr.remove_scheduled_in_catalog(catalog_name, &ids).await
})
.await
}
async fn run_orphan_cleanup(
data_path: &str,
referenced: Vec<(String, bool)>,
object_store: Arc<dyn ObjectStore>,
criteria: CleanupCriteria,
dry_run: bool,
) -> Result<Vec<String>> {
let (_, base_key) = parse_object_store_url(data_path)?;
let mut referenced_set: HashSet<ObjectPath> = HashSet::with_capacity(referenced.len());
for (path, rel) in referenced {
let abs = resolve_path(&base_key, &path, rel)?;
referenced_set.insert(ObjectPath::from(abs.trim_start_matches('/')));
}
let prefix = ObjectPath::from(base_key.trim_start_matches('/'));
let entries: Vec<object_store::ObjectMeta> =
object_store.list(Some(&prefix)).try_collect().await?;
let mut orphans: Vec<ObjectPath> = Vec::new();
for meta in entries {
if !meta.location.as_ref().ends_with(".parquet") {
continue;
}
if let CleanupCriteria::OlderThan(cutoff) = &criteria
&& meta.last_modified >= *cutoff
{
continue;
}
if !referenced_set.contains(&meta.location) {
orphans.push(meta.location);
}
}
if dry_run {
return Ok(orphans.into_iter().map(|p| format!("/{p}")).collect());
}
let mut deleted = Vec::with_capacity(orphans.len());
for orphan in orphans {
match object_store.delete(&orphan).await {
Ok(()) => {},
Err(object_store::Error::NotFound {
..
}) => {},
Err(e) => return Err(e.into()),
}
deleted.push(format!("/{orphan}"));
}
Ok(deleted)
}
#[cfg(feature = "write-sqlite")]
pub async fn delete_orphaned_files_sqlite(
writer: &crate::metadata_writer_sqlite::SqliteMetadataWriter,
object_store: Arc<dyn ObjectStore>,
criteria: CleanupCriteria,
dry_run: bool,
) -> Result<Vec<String>> {
let data_path = crate::metadata_writer::MetadataWriter::get_data_path(writer)?;
let referenced = writer.list_referenced_paths()?;
run_orphan_cleanup(&data_path, referenced, object_store, criteria, dry_run).await
}
#[cfg(feature = "write-postgres")]
pub async fn delete_orphaned_files_multicatalog(
mgr: &crate::multicatalog::MulticatalogManager,
object_store: Arc<dyn ObjectStore>,
criteria: CleanupCriteria,
dry_run: bool,
) -> Result<Vec<String>> {
let data_path = mgr.get_data_path().await?;
let referenced = mgr.list_referenced_paths().await?;
run_orphan_cleanup(&data_path, referenced, object_store, criteria, dry_run).await
}