Skip to content

Commit 661c9cb

Browse files
author
Diocrafts
committed
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
1 parent 2aeb973 commit 661c9cb

File tree

3 files changed

+66
-3
lines changed

3 files changed

+66
-3
lines changed

src/application/services/file_management_service.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::application::services::trash_service::TrashService;
88
use crate::common::errors::DomainError;
99
use crate::infrastructure::repositories::pg::file_blob_read_repository::FileBlobReadRepository;
1010
use crate::infrastructure::repositories::pg::file_blob_write_repository::FileBlobWriteRepository;
11+
use crate::infrastructure::services::thumbnail_service::ThumbnailService;
1112
use tracing::{error, info, warn};
1213
use uuid::Uuid;
1314

@@ -21,6 +22,7 @@ pub struct FileManagementService {
2122
file_repository: Arc<FileBlobWriteRepository>,
2223
file_read: Option<Arc<FileBlobReadRepository>>,
2324
trash_service: Option<Arc<TrashService>>,
25+
thumbnail_service: Option<Arc<ThumbnailService>>,
2426
}
2527

2628
impl FileManagementService {
@@ -30,6 +32,7 @@ impl FileManagementService {
3032
file_repository,
3133
file_read: None,
3234
trash_service: None,
35+
thumbnail_service: None,
3336
}
3437
}
3538

@@ -38,11 +41,13 @@ impl FileManagementService {
3841
file_repository: Arc<FileBlobWriteRepository>,
3942
trash_service: Option<Arc<TrashService>>,
4043
file_read: Option<Arc<FileBlobReadRepository>>,
44+
thumbnail_service: Option<Arc<ThumbnailService>>,
4145
) -> Self {
4246
Self {
4347
file_repository,
4448
file_read,
4549
trash_service,
50+
thumbnail_service,
4651
}
4752
}
4853

@@ -171,7 +176,14 @@ impl FileManagementUseCase for FileManagementService {
171176
}
172177

173178
async fn delete_file(&self, id: &str) -> Result<(), DomainError> {
174-
self.file_repository.delete_file(id).await
179+
self.file_repository.delete_file(id).await?;
180+
// Best-effort thumbnail cleanup
181+
if let Some(thumb) = &self.thumbnail_service {
182+
if let Err(e) = thumb.delete_thumbnails(id).await {
183+
warn!("Failed to delete thumbnails for file {}: {}", id, e);
184+
}
185+
}
186+
Ok(())
175187
}
176188

177189
async fn delete_file_owned(&self, id: &str, caller_id: Uuid) -> Result<(), DomainError> {
@@ -210,6 +222,12 @@ impl FileManagementUseCase for FileManagementService {
210222
// Step 2: Permanent delete — trigger handles blob ref_count
211223
warn!("Permanently deleting file: {}", id);
212224
self.file_repository.delete_file(id).await?;
225+
// Best-effort thumbnail cleanup
226+
if let Some(thumb) = &self.thumbnail_service {
227+
if let Err(e) = thumb.delete_thumbnails(id).await {
228+
warn!("Failed to delete thumbnails for file {}: {}", id, e);
229+
}
230+
}
213231
info!("File permanently deleted: {}", id);
214232

215233
Ok(false) // permanently deleted

src/application/services/trash_service.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::sync::Arc;
2-
use tracing::{debug, error, info, instrument};
2+
use tracing::{debug, error, info, instrument, warn};
33
use uuid::Uuid;
44

55
use crate::application::dtos::trash_dto::TrashedItemDto;
@@ -13,6 +13,7 @@ use crate::infrastructure::repositories::pg::file_blob_read_repository::FileBlob
1313
use crate::infrastructure::repositories::pg::file_blob_write_repository::FileBlobWriteRepository;
1414
use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
1515
use crate::infrastructure::repositories::pg::trash_db_repository::TrashDbRepository;
16+
use crate::infrastructure::services::thumbnail_service::ThumbnailService;
1617

1718
/**
1819
* Application service for trash operations.
@@ -40,6 +41,9 @@ pub struct TrashService {
4041
/// Port for folder operations (get folder, trash, restore, delete)
4142
folder_storage_port: Arc<FolderDbRepository>,
4243

44+
/// Thumbnail service for cleaning up thumbnails on permanent delete
45+
thumbnail_service: Option<Arc<ThumbnailService>>,
46+
4347
/// Number of days items should be kept in trash before automatic cleanup
4448
retention_days: u32,
4549
}
@@ -51,12 +55,14 @@ impl TrashService {
5155
file_write_port: Arc<FileBlobWriteRepository>,
5256
folder_storage_port: Arc<FolderDbRepository>,
5357
retention_days: u32,
58+
thumbnail_service: Option<Arc<ThumbnailService>>,
5459
) -> Self {
5560
Self {
5661
trash_repository,
5762
file_read_port,
5863
file_write_port,
5964
folder_storage_port,
65+
thumbnail_service,
6066
retention_days,
6167
}
6268
}
@@ -557,6 +563,14 @@ impl TrashUseCase for TrashService {
557563
}
558564
}
559565
}
566+
567+
// Best-effort thumbnail cleanup — thumbnails are cache
568+
// artifacts, so failure must not block file deletion.
569+
if let Some(thumb) = &self.thumbnail_service {
570+
if let Err(e) = thumb.delete_thumbnails(&file_id).await {
571+
warn!("Failed to delete thumbnails for file {}: {}", file_id, e);
572+
}
573+
}
560574
}
561575
TrashedItemType::Folder => {
562576
// Permanently delete the folder
@@ -647,6 +661,25 @@ impl TrashUseCase for TrashService {
647661
async fn empty_trash(&self, user_id: Uuid) -> Result<()> {
648662
info!("Emptying trash for user {}", user_id);
649663

664+
// Collect trashed file IDs BEFORE bulk-deleting so we can clean up
665+
// their thumbnails afterward. This is best-effort — if the query
666+
// fails we still proceed with the bulk delete.
667+
let trashed_file_ids: Vec<String> = if self.thumbnail_service.is_some() {
668+
match self.trash_repository.get_trash_items(&user_id).await {
669+
Ok(items) => items
670+
.iter()
671+
.filter(|i| matches!(i.item_type(), TrashedItemType::File))
672+
.map(|i| i.original_id().to_string())
673+
.collect(),
674+
Err(e) => {
675+
warn!("Could not list trashed items for thumbnail cleanup: {}", e);
676+
Vec::new()
677+
}
678+
}
679+
} else {
680+
Vec::new()
681+
};
682+
650683
// clear_trash() already performs bulk SQL DELETEs in 2 queries:
651684
// 1. DELETE FROM storage.files WHERE user_id = $1 AND is_trashed = TRUE
652685
// 2. DELETE FROM storage.folders WHERE user_id = $1 AND is_trashed = TRUE
@@ -659,6 +692,15 @@ impl TrashUseCase for TrashService {
659692
// Finally it clears the trash_items index for the user.
660693
self.trash_repository.clear_trash(&user_id).await?;
661694

695+
// Best-effort thumbnail cleanup for all deleted files
696+
if let Some(thumb) = &self.thumbnail_service {
697+
for file_id in &trashed_file_ids {
698+
if let Err(e) = thumb.delete_thumbnails(file_id).await {
699+
warn!("Failed to delete thumbnails for file {}: {}", file_id, e);
700+
}
701+
}
702+
}
703+
662704
info!("Trash emptied for user {}", user_id);
663705
Ok(())
664706
}

src/common/di.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ impl AppServiceFactory {
261261
repos.file_write_repository.clone(),
262262
trash_service.clone(),
263263
Some(repos.file_read_repository.clone()),
264+
Some(core.thumbnail_service.clone()),
264265
));
265266

266267
let file_use_case_factory = Arc::new(AppFileUseCaseFactory::new(
@@ -302,6 +303,7 @@ impl AppServiceFactory {
302303
pub async fn create_trash_service(
303304
&self,
304305
repos: &RepositoryServices,
306+
core: &CoreServices,
305307
) -> Option<Arc<TrashService>> {
306308
if !self.config.features.enable_trash {
307309
tracing::info!("Trash service is disabled in configuration");
@@ -317,6 +319,7 @@ impl AppServiceFactory {
317319
repos.file_write_repository.clone(),
318320
repos.folder_repository.clone(),
319321
self.config.storage.trash_retention_days,
322+
Some(core.thumbnail_service.clone()),
320323
));
321324

322325
// Initialize cleanup service (bulk-deletes expired items in 2 SQL queries)
@@ -457,7 +460,7 @@ impl AppServiceFactory {
457460
let repos = self.create_repository_services(&core, &pool);
458461

459462
// 3. Trash service (needed before application services)
460-
let trash_service = self.create_trash_service(&repos).await;
463+
let trash_service = self.create_trash_service(&repos, &core).await;
461464

462465
// 4. Application services (with trash already wired)
463466
let mut apps = self.create_application_services(&core, &repos, trash_service.clone());

0 commit comments

Comments
 (0)