Media
Updated: 2026-09-14 — post/story land in bucket
privateand are returned as presigned URLs; MediaProcessingWorker polls PostgreSQL (not Kafka).
Bellamy Book uses a Media layer for user and content assets: avatars, wallpapers (cover images), posts, stories, chat, tickets, and blog images/files. Media files are stored in MinIO or Cloudflare R2 (switchable via Storage:Provider config — see Storage Configuration). The API exposes upload endpoints and URLs for reading; MediaProcessingWorker polls pending MediaFile rows in PostgreSQL and processes uploads asynchronously (thumbnails, transcoding).
Overview
- Uploads: User avatar, wallpaper, post/story media; default avatar/wallpaper (admin). Blog images/files via
api/Blog. Chat/ticket via their controllers. - API:
api/media— upload/read helpers;api/media/performance— stats (admin). - Storage buckets:
public,private,temp,files,support,backups. Temp cleanup: TempFileCleanupJob. Full setup: Storage, R2 Setup (includes Dev vs Prod env).
Where objects end up
| Media | Final bucket | How the client loads it |
|---|---|---|
| Post image/video | private | Feed/story APIs return presigned S3 URLs |
| Story image/video | private | Same (presigned) |
| Avatar / cover | public | Direct CDN URL (R2__PublicUrl / MinIO public URL) |
| Comment / chat thumbs | public (chat often via /api/chat/media stream) | CDN or API stream |
| Blog images | public | Prefer /api/blogs/images/{id} stream; DTO may also expose CDN |
| Ticket images | public (after process); upload stages in temp | /api/tickets/images/… |
API Endpoints
Uploads (api/media)
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/media/avatar/{userId} | Upload user avatar |
| POST | /api/media/wallpaper/{userId} | Upload user cover/wallpaper |
| POST | /api/media/video/{userId} | Upload user video |
| POST | /api/media/story/{userId} | Upload story media |
| POST | /api/media/default/avatar | Upload default avatar (admin) |
| POST | /api/media/default/wallpaper | Upload default wallpaper (admin) |
Read (api/media)
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/media/avatar/{userId} | Get user avatar URL |
| GET | /api/media/wallpaper/{userId} | Get user wallpaper URL |
| GET | /api/media/{mediaFileId} | Get media file by ID |
| GET | /api/media/{mediaFileId}/status | Get processing status |
| GET | /api/media/user/{userId} | Get media files for user |
| GET | /api/media/default/avatar | Get default avatar URL |
| GET | /api/media/default/wallpaper | Get default wallpaper URL |
Performance (api/media/performance) — Admin/Monitoring
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/media/performance/stats | Aggregate media stats |
| GET | /api/media/performance/errors | Processing errors |
| GET | /api/media/performance/stats/by-type | Stats by media type |
Processing
- MediaProcessingWorker polls pending
MediaFilerows (not a Kafka consumer), generates sizes / transcodes video, writes to the final bucket, and marks status Completed. - Feed and story queries load media via
GetMediaFilesByIdsAsync/GetMediaFileByIdAsync, which presign any…/private/…URLs before returning DTOs. - TempFileCleanupJob (Quartz) deletes files in the temp bucket older than 24 hours.
If post/story stay gray: deploy an API build that includes R2 region auto + private URL presigning, and ensure the R2 private bucket exists. Cloudflare Worker update is optional for post/story (presign bypasses the Worker). See R2 Setup.