fix: delete thumbnails when files are permanently deleted
- Inject ThumbnailService into TrashService and FileManagementService - Call delete_thumbnails() after permanent file deletion in: - TrashService::delete_permanently (single item) - TrashService::empty_trash (bulk: collects file IDs first) - FileManagementService::delete_file - FileManagementService::delete_with_cleanup (fallback path) - All thumbnail cleanup is best-effort (warn on failure, never blocks) - Prevents orphaned thumbnail files from accumulating on disk
This commit is contained in:
@@ -8,6 +8,7 @@ use crate::application::services::trash_service::TrashService;
|
|||||||
use crate::common::errors::DomainError;
|
use crate::common::errors::DomainError;
|
||||||
use crate::infrastructure::repositories::pg::file_blob_read_repository::FileBlobReadRepository;
|
use crate::infrastructure::repositories::pg::file_blob_read_repository::FileBlobReadRepository;
|
||||||
use crate::infrastructure::repositories::pg::file_blob_write_repository::FileBlobWriteRepository;
|
use crate::infrastructure::repositories::pg::file_blob_write_repository::FileBlobWriteRepository;
|
||||||
|
use crate::infrastructure::services::thumbnail_service::ThumbnailService;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ pub struct FileManagementService {
|
|||||||
file_repository: Arc<FileBlobWriteRepository>,
|
file_repository: Arc<FileBlobWriteRepository>,
|
||||||
file_read: Option<Arc<FileBlobReadRepository>>,
|
file_read: Option<Arc<FileBlobReadRepository>>,
|
||||||
trash_service: Option<Arc<TrashService>>,
|
trash_service: Option<Arc<TrashService>>,
|
||||||
|
thumbnail_service: Option<Arc<ThumbnailService>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileManagementService {
|
impl FileManagementService {
|
||||||
@@ -30,6 +32,7 @@ impl FileManagementService {
|
|||||||
file_repository,
|
file_repository,
|
||||||
file_read: None,
|
file_read: None,
|
||||||
trash_service: None,
|
trash_service: None,
|
||||||
|
thumbnail_service: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,11 +41,13 @@ impl FileManagementService {
|
|||||||
file_repository: Arc<FileBlobWriteRepository>,
|
file_repository: Arc<FileBlobWriteRepository>,
|
||||||
trash_service: Option<Arc<TrashService>>,
|
trash_service: Option<Arc<TrashService>>,
|
||||||
file_read: Option<Arc<FileBlobReadRepository>>,
|
file_read: Option<Arc<FileBlobReadRepository>>,
|
||||||
|
thumbnail_service: Option<Arc<ThumbnailService>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
file_repository,
|
file_repository,
|
||||||
file_read,
|
file_read,
|
||||||
trash_service,
|
trash_service,
|
||||||
|
thumbnail_service,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +176,14 @@ impl FileManagementUseCase for FileManagementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError> {
|
async fn delete_file(&self, id: &str) -> Result<(), DomainError> {
|
||||||
self.file_repository.delete_file(id).await
|
self.file_repository.delete_file(id).await?;
|
||||||
|
// Best-effort thumbnail cleanup
|
||||||
|
if let Some(thumb) = &self.thumbnail_service {
|
||||||
|
if let Err(e) = thumb.delete_thumbnails(id).await {
|
||||||
|
warn!("Failed to delete thumbnails for file {}: {}", id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_file_owned(&self, id: &str, caller_id: Uuid) -> Result<(), DomainError> {
|
async fn delete_file_owned(&self, id: &str, caller_id: Uuid) -> Result<(), DomainError> {
|
||||||
@@ -210,6 +222,12 @@ impl FileManagementUseCase for FileManagementService {
|
|||||||
// Step 2: Permanent delete — trigger handles blob ref_count
|
// Step 2: Permanent delete — trigger handles blob ref_count
|
||||||
warn!("Permanently deleting file: {}", id);
|
warn!("Permanently deleting file: {}", id);
|
||||||
self.file_repository.delete_file(id).await?;
|
self.file_repository.delete_file(id).await?;
|
||||||
|
// Best-effort thumbnail cleanup
|
||||||
|
if let Some(thumb) = &self.thumbnail_service {
|
||||||
|
if let Err(e) = thumb.delete_thumbnails(id).await {
|
||||||
|
warn!("Failed to delete thumbnails for file {}: {}", id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
info!("File permanently deleted: {}", id);
|
info!("File permanently deleted: {}", id);
|
||||||
|
|
||||||
Ok(false) // permanently deleted
|
Ok(false) // permanently deleted
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::{debug, error, info, instrument};
|
use tracing::{debug, error, info, instrument, warn};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::application::dtos::trash_dto::TrashedItemDto;
|
use crate::application::dtos::trash_dto::TrashedItemDto;
|
||||||
@@ -13,6 +13,7 @@ use crate::infrastructure::repositories::pg::file_blob_read_repository::FileBlob
|
|||||||
use crate::infrastructure::repositories::pg::file_blob_write_repository::FileBlobWriteRepository;
|
use crate::infrastructure::repositories::pg::file_blob_write_repository::FileBlobWriteRepository;
|
||||||
use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
|
use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
|
||||||
use crate::infrastructure::repositories::pg::trash_db_repository::TrashDbRepository;
|
use crate::infrastructure::repositories::pg::trash_db_repository::TrashDbRepository;
|
||||||
|
use crate::infrastructure::services::thumbnail_service::ThumbnailService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Application service for trash operations.
|
* Application service for trash operations.
|
||||||
@@ -40,6 +41,9 @@ pub struct TrashService {
|
|||||||
/// Port for folder operations (get folder, trash, restore, delete)
|
/// Port for folder operations (get folder, trash, restore, delete)
|
||||||
folder_storage_port: Arc<FolderDbRepository>,
|
folder_storage_port: Arc<FolderDbRepository>,
|
||||||
|
|
||||||
|
/// Thumbnail service for cleaning up thumbnails on permanent delete
|
||||||
|
thumbnail_service: Option<Arc<ThumbnailService>>,
|
||||||
|
|
||||||
/// Number of days items should be kept in trash before automatic cleanup
|
/// Number of days items should be kept in trash before automatic cleanup
|
||||||
retention_days: u32,
|
retention_days: u32,
|
||||||
}
|
}
|
||||||
@@ -51,12 +55,14 @@ impl TrashService {
|
|||||||
file_write_port: Arc<FileBlobWriteRepository>,
|
file_write_port: Arc<FileBlobWriteRepository>,
|
||||||
folder_storage_port: Arc<FolderDbRepository>,
|
folder_storage_port: Arc<FolderDbRepository>,
|
||||||
retention_days: u32,
|
retention_days: u32,
|
||||||
|
thumbnail_service: Option<Arc<ThumbnailService>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
trash_repository,
|
trash_repository,
|
||||||
file_read_port,
|
file_read_port,
|
||||||
file_write_port,
|
file_write_port,
|
||||||
folder_storage_port,
|
folder_storage_port,
|
||||||
|
thumbnail_service,
|
||||||
retention_days,
|
retention_days,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -557,6 +563,14 @@ impl TrashUseCase for TrashService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Best-effort thumbnail cleanup — thumbnails are cache
|
||||||
|
// artifacts, so failure must not block file deletion.
|
||||||
|
if let Some(thumb) = &self.thumbnail_service {
|
||||||
|
if let Err(e) = thumb.delete_thumbnails(&file_id).await {
|
||||||
|
warn!("Failed to delete thumbnails for file {}: {}", file_id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
TrashedItemType::Folder => {
|
TrashedItemType::Folder => {
|
||||||
// Permanently delete the folder
|
// Permanently delete the folder
|
||||||
@@ -647,6 +661,25 @@ impl TrashUseCase for TrashService {
|
|||||||
async fn empty_trash(&self, user_id: Uuid) -> Result<()> {
|
async fn empty_trash(&self, user_id: Uuid) -> Result<()> {
|
||||||
info!("Emptying trash for user {}", user_id);
|
info!("Emptying trash for user {}", user_id);
|
||||||
|
|
||||||
|
// Collect trashed file IDs BEFORE bulk-deleting so we can clean up
|
||||||
|
// their thumbnails afterward. This is best-effort — if the query
|
||||||
|
// fails we still proceed with the bulk delete.
|
||||||
|
let trashed_file_ids: Vec<String> = if self.thumbnail_service.is_some() {
|
||||||
|
match self.trash_repository.get_trash_items(&user_id).await {
|
||||||
|
Ok(items) => items
|
||||||
|
.iter()
|
||||||
|
.filter(|i| matches!(i.item_type(), TrashedItemType::File))
|
||||||
|
.map(|i| i.original_id().to_string())
|
||||||
|
.collect(),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Could not list trashed items for thumbnail cleanup: {}", e);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
// clear_trash() already performs bulk SQL DELETEs in 2 queries:
|
// clear_trash() already performs bulk SQL DELETEs in 2 queries:
|
||||||
// 1. DELETE FROM storage.files WHERE user_id = $1 AND is_trashed = TRUE
|
// 1. DELETE FROM storage.files WHERE user_id = $1 AND is_trashed = TRUE
|
||||||
// 2. DELETE FROM storage.folders WHERE user_id = $1 AND is_trashed = TRUE
|
// 2. DELETE FROM storage.folders WHERE user_id = $1 AND is_trashed = TRUE
|
||||||
@@ -659,6 +692,15 @@ impl TrashUseCase for TrashService {
|
|||||||
// Finally it clears the trash_items index for the user.
|
// Finally it clears the trash_items index for the user.
|
||||||
self.trash_repository.clear_trash(&user_id).await?;
|
self.trash_repository.clear_trash(&user_id).await?;
|
||||||
|
|
||||||
|
// Best-effort thumbnail cleanup for all deleted files
|
||||||
|
if let Some(thumb) = &self.thumbnail_service {
|
||||||
|
for file_id in &trashed_file_ids {
|
||||||
|
if let Err(e) = thumb.delete_thumbnails(file_id).await {
|
||||||
|
warn!("Failed to delete thumbnails for file {}: {}", file_id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
info!("Trash emptied for user {}", user_id);
|
info!("Trash emptied for user {}", user_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -261,6 +261,7 @@ impl AppServiceFactory {
|
|||||||
repos.file_write_repository.clone(),
|
repos.file_write_repository.clone(),
|
||||||
trash_service.clone(),
|
trash_service.clone(),
|
||||||
Some(repos.file_read_repository.clone()),
|
Some(repos.file_read_repository.clone()),
|
||||||
|
Some(core.thumbnail_service.clone()),
|
||||||
));
|
));
|
||||||
|
|
||||||
let file_use_case_factory = Arc::new(AppFileUseCaseFactory::new(
|
let file_use_case_factory = Arc::new(AppFileUseCaseFactory::new(
|
||||||
@@ -302,6 +303,7 @@ impl AppServiceFactory {
|
|||||||
pub async fn create_trash_service(
|
pub async fn create_trash_service(
|
||||||
&self,
|
&self,
|
||||||
repos: &RepositoryServices,
|
repos: &RepositoryServices,
|
||||||
|
core: &CoreServices,
|
||||||
) -> Option<Arc<TrashService>> {
|
) -> Option<Arc<TrashService>> {
|
||||||
if !self.config.features.enable_trash {
|
if !self.config.features.enable_trash {
|
||||||
tracing::info!("Trash service is disabled in configuration");
|
tracing::info!("Trash service is disabled in configuration");
|
||||||
@@ -317,6 +319,7 @@ impl AppServiceFactory {
|
|||||||
repos.file_write_repository.clone(),
|
repos.file_write_repository.clone(),
|
||||||
repos.folder_repository.clone(),
|
repos.folder_repository.clone(),
|
||||||
self.config.storage.trash_retention_days,
|
self.config.storage.trash_retention_days,
|
||||||
|
Some(core.thumbnail_service.clone()),
|
||||||
));
|
));
|
||||||
|
|
||||||
// Initialize cleanup service (bulk-deletes expired items in 2 SQL queries)
|
// Initialize cleanup service (bulk-deletes expired items in 2 SQL queries)
|
||||||
@@ -457,7 +460,7 @@ impl AppServiceFactory {
|
|||||||
let repos = self.create_repository_services(&core, &pool);
|
let repos = self.create_repository_services(&core, &pool);
|
||||||
|
|
||||||
// 3. Trash service (needed before application services)
|
// 3. Trash service (needed before application services)
|
||||||
let trash_service = self.create_trash_service(&repos).await;
|
let trash_service = self.create_trash_service(&repos, &core).await;
|
||||||
|
|
||||||
// 4. Application services (with trash already wired)
|
// 4. Application services (with trash already wired)
|
||||||
let mut apps = self.create_application_services(&core, &repos, trash_service.clone());
|
let mut apps = self.create_application_services(&core, &repos, trash_service.clone());
|
||||||
|
|||||||
Reference in New Issue
Block a user