feat: safe deletion, album/asset delete, trash, README update
- volume-aware deletion: read-only volumes remove DB only, writable volumes soft-delete to trash with configurable grace period - trash page with restore, worker purge sweep (TRASH_RETENTION_DAYS) - album delete endpoint + sidebar trash icon - asset delete from timeline selection toolbar - all listing queries exclude trashed assets (deleted_at IS NULL) - timeline ordered by EXIF capture date, date-summary endpoint - README rewritten with features, setup, full env var table
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use application::catalog::DeleteAssetHandler;
|
||||
use application::processing::{EnqueueJobHandler, ProcessNextJobHandler};
|
||||
use domain::ports::JobRepository;
|
||||
use domain::ports::{AssetRepository, JobRepository};
|
||||
|
||||
use crate::config::WorkerConfig;
|
||||
use crate::factories::{
|
||||
@@ -12,6 +13,9 @@ pub struct WorkerServices {
|
||||
pub process_next: Arc<ProcessNextJobHandler>,
|
||||
pub enqueue: Arc<EnqueueJobHandler>,
|
||||
pub job_repo: Arc<dyn JobRepository>,
|
||||
pub asset_repo: Arc<dyn AssetRepository>,
|
||||
pub delete_handler: Arc<DeleteAssetHandler>,
|
||||
pub trash_retention_days: u64,
|
||||
pub event_consumer:
|
||||
adapters_event_transport::EventConsumerAdapter<adapters_nats::NatsMessageSource>,
|
||||
}
|
||||
@@ -27,9 +31,8 @@ pub async fn build(config: &WorkerConfig) -> anyhow::Result<WorkerServices> {
|
||||
let event_store: Arc<dyn domain::ports::EventStore> =
|
||||
Arc::new(adapters_postgres::PostgresEventStore::new(pool.clone()));
|
||||
let repos = Repos::new(pool);
|
||||
let file_storage = Arc::new(adapters_storage::LocalFileStorage::new(
|
||||
&config.storage_path,
|
||||
));
|
||||
let file_storage: Arc<dyn domain::ports::FileStoragePort> =
|
||||
Arc::new(adapters_storage::LocalFileStorage::new(&config.storage_path));
|
||||
let sidecar_writer: Arc<dyn domain::ports::SidecarWriterPort> =
|
||||
Arc::new(adapters_sidecar::XmpSidecarWriter);
|
||||
|
||||
@@ -47,7 +50,7 @@ pub async fn build(config: &WorkerConfig) -> anyhow::Result<WorkerServices> {
|
||||
Arc::new(adapters_thumbnail::ImageThumbnailGenerator);
|
||||
let registry = Arc::new(build_plugin_registry(
|
||||
&repos,
|
||||
file_storage,
|
||||
file_storage.clone(),
|
||||
sidecar_writer,
|
||||
extractor,
|
||||
thumbnail_gen,
|
||||
@@ -60,7 +63,18 @@ pub async fn build(config: &WorkerConfig) -> anyhow::Result<WorkerServices> {
|
||||
event_pub.clone(),
|
||||
));
|
||||
let job_repo: Arc<dyn JobRepository> = repos.job.clone();
|
||||
let enqueue = Arc::new(build_enqueue_handler(&repos, event_pub));
|
||||
let asset_repo: Arc<dyn AssetRepository> = repos.asset.clone();
|
||||
let enqueue = Arc::new(build_enqueue_handler(&repos, event_pub.clone()));
|
||||
|
||||
let sidecar_repo: Arc<dyn domain::ports::SidecarRepository> = repos.sidecar.clone();
|
||||
let delete_handler = Arc::new(DeleteAssetHandler::new(
|
||||
repos.asset.clone(),
|
||||
repos.volume.clone(),
|
||||
repos.derivative.clone(),
|
||||
sidecar_repo,
|
||||
file_storage,
|
||||
event_pub,
|
||||
));
|
||||
|
||||
let consumer_source = adapters_nats::NatsMessageSource::new(nats_client);
|
||||
let event_consumer = adapters_event_transport::EventConsumerAdapter::new(consumer_source);
|
||||
@@ -69,6 +83,9 @@ pub async fn build(config: &WorkerConfig) -> anyhow::Result<WorkerServices> {
|
||||
process_next,
|
||||
enqueue,
|
||||
job_repo,
|
||||
asset_repo,
|
||||
delete_handler,
|
||||
trash_retention_days: config.trash_retention_days,
|
||||
event_consumer,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ pub struct WorkerConfig {
|
||||
pub nats_url: String,
|
||||
pub fallback_sweep_secs: u64,
|
||||
pub storage_path: String,
|
||||
pub trash_retention_days: u64,
|
||||
}
|
||||
|
||||
impl WorkerConfig {
|
||||
@@ -17,6 +18,10 @@ impl WorkerConfig {
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(60),
|
||||
storage_path: std::env::var("STORAGE_PATH").unwrap_or_else(|_| "./storage".into()),
|
||||
trash_retention_days: std::env::var("TRASH_RETENTION_DAYS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(30),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,13 @@ async fn main() -> anyhow::Result<()> {
|
||||
shutdown_rx.clone(),
|
||||
));
|
||||
|
||||
tokio::spawn(sweep::purge_trash(
|
||||
services.asset_repo.clone(),
|
||||
services.delete_handler.clone(),
|
||||
services.trash_retention_days,
|
||||
shutdown_rx.clone(),
|
||||
));
|
||||
|
||||
event_loop::run(services, shutdown_rx).await;
|
||||
|
||||
info!("worker shutdown complete");
|
||||
|
||||
@@ -4,7 +4,9 @@ use std::time::Duration;
|
||||
use tokio::sync::watch;
|
||||
use tracing::{error, info};
|
||||
|
||||
use application::catalog::DeleteAssetHandler;
|
||||
use application::processing::{ProcessNextJobCommand, ProcessNextJobHandler};
|
||||
use domain::ports::AssetRepository;
|
||||
|
||||
pub async fn run(
|
||||
handler: Arc<ProcessNextJobHandler>,
|
||||
@@ -35,3 +37,37 @@ pub async fn run(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn purge_trash(
|
||||
asset_repo: Arc<dyn AssetRepository>,
|
||||
delete_handler: Arc<DeleteAssetHandler>,
|
||||
retention_days: u64,
|
||||
mut shutdown: watch::Receiver<bool>,
|
||||
) {
|
||||
let interval = Duration::from_secs(3600);
|
||||
info!(retention_days, "trash purge task started");
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = shutdown.changed() => {
|
||||
info!("trash purge: shutting down");
|
||||
break;
|
||||
}
|
||||
_ = tokio::time::sleep(interval) => {}
|
||||
}
|
||||
let cutoff = chrono::Utc::now() - chrono::Duration::days(retention_days as i64);
|
||||
match asset_repo.find_trashed_before(cutoff).await {
|
||||
Ok(assets) if assets.is_empty() => {}
|
||||
Ok(assets) => {
|
||||
info!(count = assets.len(), "trash purge: purging expired assets");
|
||||
for asset in &assets {
|
||||
if let Err(e) = delete_handler.purge(&asset.asset_id).await {
|
||||
error!(asset_id = %asset.asset_id, error = %e, "trash purge: failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = %e, "trash purge: failed to query trashed assets");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user