YourBOX Video Storage
Purpose
Section titled “Purpose”ADR-0005 commits YourBOX to a Flysystem abstracted storage layer with MinIO in the homelab and on the VPS, Azure Blob in Azure, and a local adapter for workstation development. This page documents the key layout, the upload and transcoding pipeline, the signed URL strategy and the backup model.
Adapters And Selection
Section titled “Adapters And Selection”The Symfony service App\Catalog\Application\Port\VideoStorage exposes the methods
upload, download, delete, getSignedReadUrl and exists. One Flysystem adapter
implements the port. Selection is driven by STORAGE_DSN parsing.
| Adapter | Used in | DSN scheme | Notes |
|---|---|---|---|
s3 |
Homelab, VPS | s3://... |
MinIO single node on VPS, distributed in homelab |
azure_blob |
Azure | azure-blob://... |
Geo redundant storage class |
local |
Local docker compose | file://... |
Host volume mount, never used outside dev |
The DSN is parsed by App\Catalog\Infrastructure\Storage\StorageFactory which builds
the matching Flysystem filesystem and wraps it in a FlysystemVideoStorage adapter.
Key Layout
Section titled “Key Layout”The database stores a logical storage key, not a filesystem path. The format is:
videos/<YYYY>/<MM>/<ulid>/source.<ext>videos/<YYYY>/<MM>/<ulid>/hls/index.m3u8videos/<YYYY>/<MM>/<ulid>/hls/segment_<NNN>.tsvideos/<YYYY>/<MM>/<ulid>/thumbnail.jpgvideos/<YYYY>/<MM>/<ulid>/preview.webpThe ULID encodes the upload timestamp, so even without the YYYY/MM prefix the keys
remain lexicographically sortable by upload time. The YYYY/MM prefix groups objects
by upload month to keep object listings cheap on the operator side.
A videos/orphans/ prefix collects keys that failed transcoding. The reconciliation
job documented on the runbook page sweeps this prefix daily.
Upload Flow
Section titled “Upload Flow”sequenceDiagram participant Browser participant Controller as UploadController participant Storage as VideoStorage adapter participant DB as PostgreSQL participant Bus as Messenger bus participant Worker as Transcoding worker
Browser->>Controller: POST /catalog/upload (multipart, CSRF token) Controller->>Controller: validate MIME, size, extension Controller->>Storage: upload(source key, stream) Controller->>DB: INSERT video (status=pending, source_key) Controller->>Bus: TranscodeVideo(video_id) Controller-->>Browser: 202 with status URL Bus->>Worker: dispatch Worker->>Storage: download(source key) into tmpfs Worker->>Worker: ffmpeg transcode (HLS 360p, 540p, 720p) Worker->>Storage: upload manifest and segments under hls/ prefix Worker->>Storage: upload thumbnail.jpg and preview.webp Worker->>DB: UPDATE video (status=published, manifest_key, duration_seconds) Worker->>Bus: VideoPublished(video_id)Until the video reaches status=published, the catalog hides it from public listings.
A separate admin view shows pending videos so moderators can intervene if the
transcoding stalls.
Transcoding Settings
Section titled “Transcoding Settings”The FFmpeg pipeline produces an HLS manifest with three renditions and a thumbnail
sequence. The settings live in App\Catalog\Infrastructure\Transcoding\HlsProfile.
| Rendition | Resolution | Video bitrate | Audio bitrate | Keyframe interval |
|---|---|---|---|---|
| 360p | 640x360 | 800 kbps | 96 kbps | 2 seconds |
| 540p | 960x540 | 1500 kbps | 128 kbps | 2 seconds |
| 720p | 1280x720 | 3000 kbps | 128 kbps | 2 seconds |
The thumbnail is captured at the 10 percent mark of the video, then resized to 1280x720 JPEG. A 5 second WebP preview is generated by sampling 10 frames spread across the video.
Signed URLs And Playback
Section titled “Signed URLs And Playback”Public videos serve their manifest through a signed URL valid for one hour. The browser fetches the manifest, then fetches segments with their own signed URLs included in the manifest by the adapter rewriter.
For MinIO, the adapter signs S3 presigned URLs with the storage credentials. For Azure Blob, the adapter generates SAS tokens with read permission on the manifest and segment blobs.
The getSignedReadUrl method on the storage port carries a ttl parameter. The
catalog request handler uses one hour for public videos, ten minutes for private
videos, and a single use signature for download endpoints (when added).
$manifestUrl = $this->storage->getSignedReadUrl( $video->manifestKey(), ttl: new \DateInterval('PT1H'),);HTTPS Edge
Section titled “HTTPS Edge”Traefik terminates HTTPS in front of the application on the VPS and in the homelab. Traefik does not proxy the segments; the browser hits the storage backend directly. This keeps the application out of the playback bandwidth path and lets the storage backend handle range requests natively.
In Azure, the same pattern applies through Azure Front Door or the App Service URL. The Azure Blob container exposes the segments through HTTPS with the SAS token.
Backup And Disaster Recovery
Section titled “Backup And Disaster Recovery”Backup ownership is delegated to the storage backend:
- MinIO in homelab: object replication to a secondary MinIO cluster on the trusted admin plane plus object versioning enabled for 30 days.
- MinIO on VPS smoke test: object versioning for 7 days. Recovery from a catastrophic VPS loss falls back to the homelab cluster acting as the cold copy.
- Azure Blob: geo redundant storage with read access geo replication. Soft delete retention is set to 30 days.
- Local adapter: no backup, by definition of “workstation development”. The
compose stack ships a make target
make storage:flushto clean up.
The runbook import-mysql-to-postgres documents the historical migration from the
2020 absolute path metadata to the new key layout. The runbook restore-video
documents per backend recovery for a single video.
Hardening
Section titled “Hardening”- Uploads are restricted to authenticated users with
ROLE_USER. The upload endpoint enforces CSRF, validates the MIME type againstimage/jpeg(thumbnail) andvideo/mp4|webm|quicktime|x-matroska(source), checks the magic bytes viafinfo_file, and caps the size at 2 GiB. - The Symfony controller never trusts the client provided filename. The storage key is generated server side and the client name is stored as a separate display metadata field.
- The MinIO root credentials live in OpenBao under
kv/yourbox/minioand never touch the application image. The application uses a scoped IAM policy that grantss3:PutObject,s3:GetObjectands3:DeleteObjectonarn:aws:s3:::yourbox/*only. - Range request bypass attacks are mitigated by the storage backend signature validation. A signed URL is bound to a specific key prefix.