fix(backend): invalidate thumbnail cache on replace and permanent delete
Replacing a file's content left the old {id}_thumb.jpg / {id}_preview.jpg
in the cache, and the cache-hit fast path kept serving the stale image
forever; permanent deletion left those files orphaned. FileStorage gains
InvalidateCache, which Replace and PermanentDelete now call.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user