profile_image_file_id on member table, entity_type=member category=profile in file storage. UI shows silhouette placeholder when empty.
337 lines
9.8 KiB
Markdown
337 lines
9.8 KiB
Markdown
Music Store Management Platform
|
|
|
|
File Storage Architecture
|
|
|
|
Version 1.0 | Draft
|
|
|
|
|
|
|
|
# 1. Overview
|
|
|
|
The platform requires file storage for images, PDFs, and documents across many domains: member identity documents, product photos, rental agreement PDFs, repair ticket photos, signature captures, barcode labels, and reports. This document defines a storage abstraction layer that supports both local filesystem and S3-compatible backends, with integrated backup/restore.
|
|
|
|
|
|
|
|
# 2. Storage Provider Interface
|
|
|
|
The storage layer is abstracted behind a `StorageProvider` interface. The application never writes to disk or S3 directly — all file operations go through the provider. This allows stores to choose their storage backend without code changes.
|
|
|
|
## 2.1 Providers
|
|
|
|
Provider | Backend | Best For
|
|
local | Local filesystem directory | Self-hosted stores, Docker volume mounts, bare metal installs
|
|
s3 | S3-compatible API | SaaS deployment, AWS S3, MinIO, DigitalOcean Spaces, Cloudflare R2
|
|
|
|
## 2.2 Configuration
|
|
|
|
Variable | Description | Default
|
|
STORAGE_PROVIDER | local or s3 | local
|
|
STORAGE_LOCAL_PATH | Filesystem path for local provider | ./data/files
|
|
S3_BUCKET | S3 bucket name | (required if s3)
|
|
S3_REGION | AWS region or endpoint region | us-east-1
|
|
S3_ENDPOINT | Custom endpoint for non-AWS S3 (MinIO, DO, R2) | (optional)
|
|
S3_ACCESS_KEY | Access key | (required if s3)
|
|
S3_SECRET_KEY | Secret key | (required if s3)
|
|
|
|
## 2.3 Interface
|
|
|
|
```typescript
|
|
interface StorageProvider {
|
|
put(path: string, data: Buffer, contentType: string): Promise<void>
|
|
get(path: string): Promise<Buffer>
|
|
delete(path: string): Promise<void>
|
|
exists(path: string): Promise<boolean>
|
|
getUrl(path: string, expiresIn?: number): Promise<string>
|
|
}
|
|
```
|
|
|
|
- `put` — write a file
|
|
- `get` — read a file
|
|
- `delete` — remove a file
|
|
- `exists` — check if a file exists
|
|
- `getUrl` — get a URL to serve the file
|
|
- Local: returns `/v1/files/{path}` (served by Fastify)
|
|
- S3: returns a presigned URL (default 1 hour expiry)
|
|
|
|
|
|
|
|
# 3. File Table Schema
|
|
|
|
All files are tracked in a central `file` table. Domain tables reference files by UUID foreign key — never by URL, path, or base64.
|
|
|
|
## 3.1 file
|
|
|
|
Column | Type | Notes
|
|
id | uuid PK |
|
|
company_id | uuid FK | Tenant scoping
|
|
path | varchar | Relative path within storage root (e.g. `{company_id}/members/{member_id}/identifiers/{id}/front.jpg`)
|
|
filename | varchar | Original filename from upload
|
|
content_type | varchar | MIME type (image/jpeg, application/pdf, etc.)
|
|
size_bytes | integer | File size in bytes
|
|
entity_type | varchar | What this file belongs to: member_identifier, product, rental_agreement, repair_ticket, etc.
|
|
entity_id | uuid | ID of the owning record
|
|
category | varchar | Purpose within entity: front, back, signature, intake, completed, primary, thumbnail, etc.
|
|
uploaded_by | uuid FK | Employee who uploaded
|
|
created_at | timestamptz |
|
|
|
|
## 3.2 Indexes
|
|
|
|
- `(company_id, entity_type, entity_id)` — find all files for a record
|
|
- `(company_id, path)` — unique, prevents duplicates
|
|
|
|
|
|
|
|
# 4. File Path Convention
|
|
|
|
Files are organized by company and entity type for easy browsing and backup:
|
|
|
|
```
|
|
{company_id}/
|
|
members/{member_id}/
|
|
identifiers/{identifier_id}/
|
|
front.jpg
|
|
back.jpg
|
|
products/{product_id}/
|
|
primary.jpg
|
|
gallery-{n}.jpg
|
|
thumbnail.jpg
|
|
rentals/{rental_id}/
|
|
agreements/{agreement_id}.pdf
|
|
signatures/{agreement_id}.png
|
|
repairs/{ticket_id}/
|
|
intake-{n}.jpg
|
|
completed-{n}.jpg
|
|
invoice.pdf
|
|
labels/
|
|
batch-{batch_id}.pdf
|
|
reports/
|
|
{report_type}-{date}.pdf
|
|
```
|
|
|
|
Path is always relative to the storage root. The storage provider prepends the root path or S3 bucket prefix.
|
|
|
|
|
|
|
|
# 5. Upload API
|
|
|
|
## 5.1 Upload Endpoint
|
|
|
|
POST /v1/files/upload
|
|
|
|
Multipart form data:
|
|
- `file` — the file (required)
|
|
- `entityType` — string (required)
|
|
- `entityId` — uuid (required)
|
|
- `category` — string (required)
|
|
|
|
Response: the created file record (id, path, contentType, sizeBytes, url)
|
|
|
|
## 5.2 Limits
|
|
|
|
Limit | Value
|
|
Max file size | 10 MB (images), 25 MB (PDFs)
|
|
Allowed types | image/jpeg, image/png, image/webp, application/pdf
|
|
Max files per entity | 20
|
|
|
|
## 5.3 Image Processing
|
|
|
|
On upload of image files:
|
|
- Validate dimensions and format
|
|
- Generate thumbnail (200x200, cover crop) stored as a sibling file with category `thumbnail`
|
|
- Strip EXIF data for privacy (GPS coordinates, camera info)
|
|
- Convert to JPEG if PNG and > 1MB (with quality 85)
|
|
|
|
No processing for PDFs.
|
|
|
|
|
|
|
|
# 6. Serving Files
|
|
|
|
## 6.1 Download Endpoint
|
|
|
|
GET /v1/files/{fileId}
|
|
|
|
- Authenticated — requires valid JWT
|
|
- Returns the file with correct Content-Type header
|
|
- Local provider: reads from disk, streams to response
|
|
- S3 provider: redirects to presigned URL (302)
|
|
|
|
## 6.2 Thumbnail Shortcut
|
|
|
|
GET /v1/files/{fileId}/thumbnail
|
|
|
|
Returns the thumbnail version if it exists, otherwise the original.
|
|
|
|
## 6.3 Listing Files
|
|
|
|
GET /v1/files?entityType=product&entityId={uuid}
|
|
|
|
Returns all files for a given entity. Response: `{ data: File[] }`
|
|
|
|
|
|
|
|
# 7. Backup & Restore
|
|
|
|
## 7.1 Backup Command
|
|
|
|
```bash
|
|
forte-backup [--output backup.tar.gz]
|
|
```
|
|
|
|
Steps:
|
|
1. `pg_dump` the database → `backup/db.sql`
|
|
2. Copy the storage directory → `backup/files/`
|
|
3. Write metadata (version, date, provider) → `backup/manifest.json`
|
|
4. Compress → `forte-backup-{YYYY-MM-DD}.tar.gz`
|
|
|
|
For S3 provider: syncs the bucket to a local temp directory first, then archives.
|
|
|
|
## 7.2 Restore Command
|
|
|
|
```bash
|
|
forte-restore backup.tar.gz [--target-db forte] [--target-storage ./data/files]
|
|
```
|
|
|
|
Steps:
|
|
1. Extract archive
|
|
2. Drop and recreate target database
|
|
3. `psql < db.sql` to restore schema and data
|
|
4. Copy `files/` to storage root (or upload to S3 bucket)
|
|
5. Verify file count matches database records
|
|
|
|
## 7.3 Manifest
|
|
|
|
```json
|
|
{
|
|
"version": "1.0",
|
|
"date": "2026-03-28T12:00:00Z",
|
|
"database": "forte",
|
|
"storageProvider": "local",
|
|
"fileCount": 1234,
|
|
"totalSizeBytes": 524288000,
|
|
"forteVersion": "0.1.0"
|
|
}
|
|
```
|
|
|
|
|
|
|
|
# 8. Domain Integration
|
|
|
|
## 8.1 Which Domains Use File Storage
|
|
|
|
Domain | Entity Type | Categories | Typical Files
|
|
Members | member | profile | Profile photo (1 per member). UI shows silhouette placeholder when empty.
|
|
Member Identifiers | member_identifier | front, back | DL/passport/school ID photos (2 per ID)
|
|
Products | product | primary, gallery, thumbnail | Product catalog photos (1-5 per product)
|
|
Rental Agreements | rental_agreement | document, signature, guardian_signature | Signed PDF + signature images
|
|
Repair Tickets | repair_ticket | intake, in_progress, completed, invoice | Condition photos + invoice PDF
|
|
Batch Repairs | repair_batch | summary, invoice | Batch summary PDF
|
|
Delivery | delivery_event | proof, signature | Delivery proof photos + signature
|
|
Barcode Labels | label_batch | labels | Generated label PDFs
|
|
Reports | report | report | Exported report PDFs
|
|
Consignment | consignment_detail | agreement | Consignment agreement PDF
|
|
|
|
## 8.2 Schema Changes
|
|
|
|
### member — Add Profile Photo
|
|
|
|
Column | Type | Notes
|
|
profile_image_file_id | uuid FK | References file.id — profile photo. UI shows silhouette placeholder when null.
|
|
|
|
### member_identifier — Replace Base64 Columns
|
|
|
|
Remove `image_front` (text) and `image_back` (text) columns. Replace with:
|
|
|
|
Column | Type | Notes
|
|
image_front_file_id | uuid FK | References file.id — front image
|
|
image_back_file_id | uuid FK | References file.id — back image
|
|
|
|
### product — Add Image Support
|
|
|
|
Column | Type | Notes
|
|
primary_image_file_id | uuid FK | References file.id — main product photo
|
|
|
|
Additional images stored via the `file` table with entity_type = product.
|
|
|
|
### rental_agreement — Document Storage
|
|
|
|
Column | Type | Notes
|
|
document_file_id | uuid FK | References file.id — signed PDF
|
|
signature_file_id | uuid FK | References file.id — signature image
|
|
guardian_signature_file_id | uuid FK | References file.id — guardian signature (if minor)
|
|
|
|
|
|
|
|
# 9. Docker Compose — Dev Storage
|
|
|
|
## 9.1 Local Provider (Default)
|
|
|
|
No additional services needed. Files stored in `./data/files/` volume:
|
|
|
|
```yaml
|
|
services:
|
|
api:
|
|
volumes:
|
|
- ./data/files:/app/data/files
|
|
environment:
|
|
- STORAGE_PROVIDER=local
|
|
- STORAGE_LOCAL_PATH=/app/data/files
|
|
```
|
|
|
|
## 9.2 MinIO for S3 Testing (Optional)
|
|
|
|
```yaml
|
|
services:
|
|
minio:
|
|
image: minio/minio:latest
|
|
ports:
|
|
- "9000:9000"
|
|
- "9001:9001"
|
|
volumes:
|
|
- minio-data:/data
|
|
environment:
|
|
- MINIO_ROOT_USER=minioadmin
|
|
- MINIO_ROOT_PASSWORD=minioadmin
|
|
command: server /data --console-address ":9001"
|
|
|
|
api:
|
|
environment:
|
|
- STORAGE_PROVIDER=s3
|
|
- S3_ENDPOINT=http://minio:9000
|
|
- S3_BUCKET=forte
|
|
- S3_REGION=us-east-1
|
|
- S3_ACCESS_KEY=minioadmin
|
|
- S3_SECRET_KEY=minioadmin
|
|
```
|
|
|
|
|
|
|
|
# 10. Implementation Order
|
|
|
|
1. Create `file` table and migration
|
|
2. Implement `StorageProvider` interface with `LocalProvider`
|
|
3. Add `@fastify/multipart` for file uploads
|
|
4. Create upload endpoint (POST /v1/files/upload)
|
|
5. Create download endpoint (GET /v1/files/:id)
|
|
6. Create list endpoint (GET /v1/files?entityType=&entityId=)
|
|
7. Add image processing (thumbnails, EXIF strip) via sharp
|
|
8. Update member_identifier to use file references
|
|
9. Implement `S3Provider`
|
|
10. Add backup/restore CLI commands
|
|
11. Add to Docker Compose dev config
|
|
|
|
|
|
|
|
# 11. Business Rules
|
|
|
|
- Files are always tenant-scoped by company_id — no cross-company access
|
|
- Deleting an entity should soft-delete its files (mark inactive, don't remove from storage)
|
|
- Hard delete of files only via backup restore or explicit admin action
|
|
- File paths must not contain user input — generated from UUIDs only
|
|
- All uploads require authentication
|
|
- Image uploads are validated for content type (magic bytes, not just extension)
|
|
- PDFs are validated for structure (not just MIME type)
|
|
- Thumbnails generated server-side — frontend never resizes
|
|
- Storage provider is set at startup — cannot be changed at runtime
|
|
- Backup includes ALL files regardless of soft-delete status
|