use crate::config::Config;
use crate::error::Result;
use crate::types::{DownloadId, Event};
use std::path::{Path, PathBuf};
use tokio::sync::broadcast;
use tracing::{debug, info, warn};
pub(crate) async fn run_cleanup_stage(
download_id: DownloadId,
download_path: &Path,
event_tx: &broadcast::Sender<Event>,
config: &Config,
) -> Result<()> {
debug!(
download_id = download_id.0,
?download_path,
"running cleanup stage"
);
event_tx.send(Event::Cleaning { id: download_id }).ok();
if !config.processing.cleanup.enabled {
debug!(download_id = download_id.0, "cleanup disabled, skipping");
return Ok(());
}
cleanup(download_id, download_path, config).await
}
async fn cleanup(
download_id: DownloadId,
download_path: &Path,
config: &Config,
) -> Result<()> {
use tokio::fs;
debug!(
download_id = download_id.0,
?download_path,
"cleaning up intermediate files"
);
if fs::metadata(download_path).await.is_err() {
debug!(
download_id = download_id.0,
?download_path,
"download path does not exist, skipping cleanup"
);
return Ok(());
}
let target_extensions: Vec<&str> = config
.processing
.cleanup
.target_extensions
.iter()
.chain(config.processing.cleanup.archive_extensions.iter())
.map(|ext| ext.as_str())
.collect();
let mut files_to_delete = Vec::new();
let mut folders_to_delete = Vec::new();
collect_cleanup_targets(
download_path,
&target_extensions,
&mut files_to_delete,
&mut folders_to_delete,
config,
)
.await;
let mut deleted_files = 0;
for file in &files_to_delete {
match fs::remove_file(file).await {
Ok(_) => {
debug!(
download_id = download_id.0,
?file,
"deleted intermediate file"
);
deleted_files += 1;
}
Err(e) => {
warn!(download_id = download_id.0, ?file, error = %e, "failed to delete file");
}
}
}
let mut deleted_folders = 0;
for folder in &folders_to_delete {
match fs::remove_dir_all(folder).await {
Ok(_) => {
debug!(
download_id = download_id.0,
?folder,
"deleted sample folder"
);
deleted_folders += 1;
}
Err(e) => {
warn!(download_id = download_id.0, ?folder, error = %e, "failed to delete folder");
}
}
}
info!(
download_id = download_id.0,
deleted_files, deleted_folders, "cleanup complete"
);
Ok(())
}
fn collect_cleanup_targets<'a>(
path: &'a Path,
target_extensions: &'a [&'a str],
files_to_delete: &'a mut Vec<PathBuf>,
folders_to_delete: &'a mut Vec<PathBuf>,
config: &'a Config,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'a>> {
Box::pin(async move {
use tokio::fs;
let is_dir = fs::metadata(path)
.await
.map(|m| m.is_dir())
.unwrap_or(false);
if is_dir
&& config.processing.cleanup.delete_samples
&& let Some(folder_name) = path.file_name().and_then(|n| n.to_str())
{
let is_sample = config
.processing
.cleanup
.sample_folder_names
.iter()
.any(|sample_name| folder_name.eq_ignore_ascii_case(sample_name));
if is_sample {
folders_to_delete.push(path.to_path_buf());
return; }
}
let mut entries = match fs::read_dir(path).await {
Ok(entries) => entries,
Err(e) => {
warn!(?path, error = %e, "failed to read directory during cleanup");
return;
}
};
while let Ok(Some(entry)) = entries.next_entry().await {
let entry_path = entry.path();
let file_type = match entry.file_type().await {
Ok(ft) => ft,
Err(_) => continue,
};
if file_type.is_file()
&& let Some(extension) = entry_path.extension().and_then(|e| e.to_str())
&& target_extensions
.iter()
.any(|ext| ext.eq_ignore_ascii_case(extension))
{
files_to_delete.push(entry_path);
} else if file_type.is_dir() {
collect_cleanup_targets(
&entry_path,
target_extensions,
files_to_delete,
folders_to_delete,
config,
)
.await;
}
}
})
}