feat(backend): implement DiskStorage with on-demand thumbnail/preview cache

Files stored as {files_path}/{id} (no extension). The ext parameter
is removed from Save/Read/Delete in both the port interface and
the implementation.

Thumbnail and Preview both use imaging.Thumbnail (fit within
configured max bounds, never upscale, never crop) — the config
values THUMB_WIDTH/HEIGHT and PREVIEW_WIDTH/HEIGHT are upper limits,
not forced dimensions.

Non-decodable files (video, etc.) receive a #444455 placeholder.
Cache writes use atomic temp→rename; on cache failure the generated
image is served from memory so the request still succeeds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 18:11:54 +03:00
parent 1a873949f4
commit fae87ad05c
4 changed files with 234 additions and 4 deletions
+4 -4
View File
@@ -11,15 +11,15 @@ import (
// thumbnails, and previews.
type FileStorage interface {
// Save writes the reader's content to storage and returns the number of
// bytes written. ext is the file extension without a leading dot (e.g. "jpg").
Save(ctx context.Context, id uuid.UUID, ext string, r io.Reader) (int64, error)
// bytes written.
Save(ctx context.Context, id uuid.UUID, r io.Reader) (int64, error)
// Read opens the file content for reading. The caller must close the returned
// ReadCloser.
Read(ctx context.Context, id uuid.UUID, ext string) (io.ReadCloser, error)
Read(ctx context.Context, id uuid.UUID) (io.ReadCloser, error)
// Delete removes the file content from storage.
Delete(ctx context.Context, id uuid.UUID, ext string) error
Delete(ctx context.Context, id uuid.UUID) error
// Thumbnail opens the pre-generated thumbnail (JPEG). Returns ErrNotFound
// if the thumbnail has not been generated yet.