Uploads
Overview
The Uploads module provides file upload and management for Lumio accounts. Files are uploaded via multipart REST endpoints and stored in a configurable storage backend (e.g., S3-compatible object storage). Upload metadata (filename, content type, size, storage key, purpose) is tracked in PostgreSQL. The module enforces plan-based limits on maximum file size and total storage quota. Download URLs are generated as presigned URLs with configurable expiry.
Architecture
Backend
- GraphQL (
apps/api/src/graphql/uploads.rs) -- Queries for listing and fetching uploads. Mutation for deleting uploads (metadata only; storage cleanup is handled by the REST endpoint or background job). - REST (
apps/api/src/routes/uploads.rs) -- Full REST CRUD: list uploads, get upload with presigned download URL, multipart file upload, and delete (storage + metadata). - Storage -- Configurable storage backend accessible via
AppState.storage. Supportsupload(),presigned_download_url(), anddelete()operations. - Database (
apps/api/src/db/uploads.rs) -- PostgreSQL operations for upload metadata CRUD.
Frontend
- File manager with upload list, size display, and download links.
- Drag-and-drop file upload with progress indicator.
- Files are referenced by other features (e.g., overlay custom assets, alert sounds/images).
API
GraphQL Queries
| Query | Permission | Description |
|---|---|---|
uploads | uploads:read | List all uploads for the account. Includes a generated downloadUrl pointing to the REST endpoint. |
upload(id: UUID) | uploads:read | Get a single upload by ID (account-scoped) |
GraphQL Mutations
| Mutation | Permission | Description |
|---|---|---|
deleteUpload(id: UUID) | uploads:delete | Delete upload metadata from database. Note: storage cleanup is handled separately. |
REST Endpoints
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /v1/uploads | uploads:read | List all uploads for the account |
GET | /v1/uploads/{id} | uploads:read | Get upload details with presigned download URL (1-hour expiry) |
POST | /v1/uploads | uploads:create | Upload a file via multipart form. Field name: file (required), purpose (optional). Enforces max upload size from config. |
DELETE | /v1/uploads/{id} | uploads:delete | Delete upload: removes file from storage (best effort) and metadata from database |
Upload Request Format
Multipart form data with fields:
file(required) -- The file to upload. Filename and content type are extracted from the content disposition.purpose(optional) -- Purpose label for the upload (e.g., "alert_sound", "overlay_image").
Storage Key Format
{account_id}/{uuid_v7}.{extension}
Example: 550e8400-e29b-41d4-a716-446655440000/01912345-6789-7abc-def0-123456789abc.png
Permissions
| Permission | Description |
|---|---|
uploads:read | View and download uploaded files |
uploads:create | Upload new files |
uploads:delete | Delete uploaded files |
Database
| Table | Database | Description |
|---|---|---|
uploads | PostgreSQL | id, account_id, uploaded_by (user UUID), filename, content_type, size_bytes, storage_key, purpose, created_at |
Data Flow
Upload Flow
- Client sends a multipart POST to
/v1/uploadswith the file and optional purpose. - Server reads the file data in chunks, checking against
max_upload_size_bytesduring upload. - A unique storage key is generated:
{account_id}/{uuid_v7}.{ext}. - File is uploaded to the storage backend via
storage.upload(). - Metadata (filename, content_type, size, storage_key, purpose) is saved to PostgreSQL.
- Response includes the upload metadata.
Download Flow
- Client requests
GET /v1/uploads/{id}. - Server verifies account ownership.
- If a storage backend is available, a presigned download URL is generated with 1-hour expiry.
- The upload metadata and download URL are returned.
Delete Flow
- Client requests
DELETE /v1/uploads/{id}. - Server verifies account ownership.
- File is deleted from storage (best effort -- logged if it fails).
- Metadata is deleted from PostgreSQL.
Plan-Based Limits
| Limit | Free | Pro | Enterprise |
|---|---|---|---|
| Max upload size | 5 MB | Higher | Higher |
| Max total storage | 100 MB | Higher | Higher |
The max_upload_size_bytes is checked during the upload stream, so oversized files are rejected early without consuming full bandwidth.
Key Files
| Path | Description |
|---|---|
apps/api/src/graphql/uploads.rs | GraphQL queries and mutations |
apps/api/src/routes/uploads.rs | REST endpoints (list, get, upload, delete) |
apps/api/src/db/uploads.rs | Database operations for upload metadata |
apps/api/src/state.rs | AppState.storage -- storage backend reference |