diff --git a/backend/internal/port/storage.go b/backend/internal/port/storage.go index 480e2b5..a97287f 100644 --- a/backend/internal/port/storage.go +++ b/backend/internal/port/storage.go @@ -21,6 +21,10 @@ type FileStorage interface { // Delete removes the file content from storage. Delete(ctx context.Context, id uuid.UUID) error + // InvalidateCache removes any cached thumbnail/preview for the file so they + // are regenerated from the current content on next request. + InvalidateCache(ctx context.Context, id uuid.UUID) error + // Thumbnail opens the pre-generated thumbnail (JPEG). Returns ErrNotFound // if the thumbnail has not been generated yet. Thumbnail(ctx context.Context, id uuid.UUID) (io.ReadCloser, error) diff --git a/backend/internal/service/file_service.go b/backend/internal/service/file_service.go index f58e26d..5c56ce7 100644 --- a/backend/internal/service/file_service.go +++ b/backend/internal/service/file_service.go @@ -345,6 +345,7 @@ func (s *FileService) PermanentDelete(ctx context.Context, id uuid.UUID) error { return err } _ = s.storage.Delete(ctx, id) + _ = s.storage.InvalidateCache(ctx, id) objType := fileObjectType _ = s.audit.Log(ctx, "file_permanent_delete", &objType, &id, nil) @@ -383,6 +384,8 @@ func (s *FileService) Replace(ctx context.Context, id uuid.UUID, p UploadParams) if _, err := s.storage.Save(ctx, id, bytes.NewReader(data)); err != nil { return nil, fmt.Errorf("FileService.Replace: save to storage: %w", err) } + // Drop stale thumbnail/preview so they regenerate from the new content. + _ = s.storage.InvalidateCache(ctx, id) patch := &domain.File{ MIMEType: mime.Name, diff --git a/backend/internal/storage/disk.go b/backend/internal/storage/disk.go index dd443e3..346a577 100644 --- a/backend/internal/storage/disk.go +++ b/backend/internal/storage/disk.go @@ -109,6 +109,17 @@ func (s *DiskStorage) Delete(_ context.Context, id uuid.UUID) error { return nil } +// InvalidateCache removes the cached thumbnail and preview for id, if present, +// so they are regenerated from the current file content on the next request. +func (s *DiskStorage) InvalidateCache(_ context.Context, id uuid.UUID) error { + for _, p := range []string{s.thumbCachePath(id), s.previewCachePath(id)} { + if err := os.Remove(p); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("storage.InvalidateCache remove %q: %w", p, err) + } + } + return nil +} + // Thumbnail returns a JPEG that fits within the configured max width×height // (never upscaled, never cropped). Generated on first call and cached. // Video files are thumbnailed via ffmpeg; other non-image files get a placeholder.