veza/FILE_UPLOAD_FORMAT.md

469 lines
12 KiB
Markdown

# File Upload Format Standardization
## INT-015: Add file upload format standardization
**Date**: 2025-12-25
**Status**: Completed
## Overview
This document defines the standardized format for all file uploads in the Veza platform. It ensures consistency between backend and frontend, making upload handling predictable and maintainable.
## Standard Upload Request Format
All file uploads use `multipart/form-data` with the following standardized field names:
### Required Fields
- **`file`** (file, required): The actual file being uploaded
### Optional Metadata Fields
- **`title`** (string, optional): Title of the content
- **`artist`** (string, optional): Artist name (for audio files)
- **`album`** (string, optional): Album name (for audio files)
- **`genre`** (string, optional): Genre
- **`year`** (integer, optional): Year
- **`description`** (string, optional): Description
### File Type and Context Fields
- **`file_type`** (string, optional): Type of file - `"audio"`, `"image"`, or `"video"` (auto-detected if not provided)
- **`track_id`** (UUID, optional): Track ID if updating an existing track
- **`is_public`** (boolean, optional): Public visibility (default: `false`)
- **`metadata`** (string, optional): JSON string with additional metadata
### Example Request
```javascript
const formData = new FormData();
formData.append('file', file);
formData.append('title', 'My Track');
formData.append('artist', 'Artist Name');
formData.append('album', 'Album Name');
formData.append('genre', 'Electronic');
formData.append('year', '2025');
formData.append('is_public', 'true');
await apiClient.post('/tracks', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
```
## Standard Upload Response Format
All upload responses follow this standardized structure:
```json
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"track_id": "660e8400-e29b-41d4-a716-446655440001",
"file_name": "track.mp3",
"file_size": 5242880,
"file_type": "audio",
"mime_type": "audio/mpeg",
"checksum": "sha256:abc123...",
"status": "completed",
"progress": 100,
"bytes_uploaded": 5242880,
"url": "https://cdn.example.com/tracks/550e8400...",
"thumbnail_url": "https://cdn.example.com/thumbnails/550e8400...",
"storage_path": "tracks/2025/12/550e8400...",
"storage_provider": "s3",
"is_processed": true,
"processed_at": "2025-12-25T10:30:00Z",
"processing_error": null,
"virus_scanned": true,
"virus_scan_result": "clean",
"virus_scanned_at": "2025-12-25T10:29:45Z",
"created_at": "2025-12-25T10:29:30Z",
"updated_at": "2025-12-25T10:30:00Z",
"expires_at": null
}
}
```
### Response Fields
- **`id`** (UUID): Upload ID
- **`track_id`** (UUID, optional): Track ID (if applicable)
- **`file_name`** (string): Original filename
- **`file_size`** (int64): File size in bytes
- **`file_type`** (string): File type (`"audio"`, `"image"`, `"video"`)
- **`mime_type`** (string): MIME type
- **`checksum`** (string): File checksum (SHA-256)
- **`status`** (string): Upload status (`"pending"`, `"uploading"`, `"processing"`, `"completed"`, `"failed"`, `"cancelled"`)
- **`progress`** (int): Progress percentage (0-100)
- **`bytes_uploaded`** (int64): Bytes uploaded so far
- **`url`** (string): Public URL (if available)
- **`thumbnail_url`** (string, optional): Thumbnail URL (if applicable)
- **`storage_path`** (string): Storage path
- **`storage_provider`** (string): Storage provider (`"s3"`, `"local"`, etc.)
- **`is_processed`** (bool): Whether file has been processed
- **`processed_at`** (string, optional): Processing completion time (ISO 8601)
- **`processing_error`** (string, optional): Processing error (if any)
- **`virus_scanned`** (bool): Whether file was scanned
- **`virus_scan_result`** (string, optional): Scan result (`"clean"`, `"infected"`, `"error"`)
- **`virus_scanned_at`** (string, optional): Scan timestamp (ISO 8601)
- **`created_at`** (string): Upload creation time (ISO 8601)
- **`updated_at`** (string): Last update time (ISO 8601)
- **`expires_at`** (string, optional): Expiration time (ISO 8601, if applicable)
## Upload Status Values
| Status | Description |
|--------|-------------|
| `pending` | Upload queued, not started |
| `uploading` | File is being uploaded |
| `processing` | File is being processed (transcoding, etc.) |
| `completed` | Upload and processing completed successfully |
| `failed` | Upload or processing failed |
| `cancelled` | Upload was cancelled |
## Error Response Format
Upload errors follow the standard error response format:
```json
{
"success": false,
"error": {
"code": "FILE_TOO_LARGE",
"message": "File size exceeds maximum allowed size of 100MB",
"details": {
"file_size": 104857600,
"max_size": 100000000,
"field": "file"
}
}
}
```
### Error Codes
| Code | HTTP Status | Description |
|------|-------------|-------------|
| `FILE_REQUIRED` | 400 | No file provided |
| `FILE_TOO_LARGE` | 413 | File size exceeds maximum |
| `INVALID_FILE_TYPE` | 415 | File type not supported |
| `INVALID_FILE_FORMAT` | 400 | File format is invalid |
| `VIRUS_DETECTED` | 422 | Virus detected in file |
| `VIRUS_SCAN_FAILED` | 503 | Virus scan failed |
| `VIRUS_SCAN_UNAVAILABLE` | 503 | Virus scanning service unavailable |
| `QUOTA_EXCEEDED` | 403 | Upload quota exceeded |
| `UPLOAD_FAILED` | 500 | Upload failed |
| `PROCESSING_FAILED` | 500 | Processing failed |
| `TOO_MANY_CONCURRENT_UPLOADS` | 503 | Too many concurrent uploads |
| `INVALID_METADATA` | 400 | Invalid metadata format |
## File Type Limits
### Audio Files
- **Max Size**: 100MB (104,857,600 bytes)
- **Allowed MIME Types**:
- `audio/mpeg`
- `audio/mp3`
- `audio/wav`
- `audio/flac`
- `audio/aac`
- `audio/ogg`
- `audio/m4a`
- **Allowed Extensions**: `.mp3`, `.wav`, `.flac`, `.aac`, `.ogg`, `.m4a`
### Image Files
- **Max Size**: 10MB (10,485,760 bytes)
- **Allowed MIME Types**:
- `image/jpeg`
- `image/png`
- `image/gif`
- `image/webp`
- `image/svg+xml`
- **Allowed Extensions**: `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.svg`
### Video Files
- **Max Size**: 500MB (524,288,000 bytes)
- **Allowed MIME Types**:
- `video/mp4`
- `video/webm`
- `video/ogg`
- `video/avi`
- **Allowed Extensions**: `.mp4`, `.webm`, `.ogg`, `.avi`
## Upload Progress
For long-running uploads, progress can be tracked:
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "uploading",
"progress": 45,
"bytes_uploaded": 2359296,
"total_bytes": 5242880,
"estimated_time_remaining": 30,
"message": "Uploading...",
"updated_at": "2025-12-25T10:30:00Z"
}
```
## Batch Upload
For multiple file uploads:
```json
{
"total_files": 5,
"successful": 4,
"failed": 1,
"results": [
{
"index": 1,
"file_name": "track1.mp3",
"file_size": 5242880,
"file_type": "audio",
"status": "completed",
"upload_id": "550e8400-e29b-41d4-a716-446655440000"
},
{
"index": 2,
"file_name": "track2.mp3",
"file_size": 10485760,
"file_type": "audio",
"status": "failed",
"error": "File too large"
}
],
"errors": []
}
```
## Backend Implementation
### Using Standard Types
```go
import "veza-backend-api/internal/upload"
// Parse upload request
var req upload.StandardUploadRequest
if err := c.ShouldBind(&req); err != nil {
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, err.Error()))
return
}
// Create response
response := &upload.StandardUploadResponse{
ID: uploadID,
FileName: fileHeader.Filename,
FileSize: fileSize,
FileType: "audio",
Status: upload.UploadStatusCompleted,
CreatedAt: time.Now(),
}
RespondSuccess(c, http.StatusCreated, response)
```
### Error Handling
```go
// Return standardized error
RespondWithAppError(c, apperrors.New(
apperrors.ErrCodeValidation,
"File size exceeds maximum allowed size",
map[string]interface{}{
"code": upload.ErrorCodeFileTooLarge,
"file_size": fileSize,
"max_size": maxSize,
},
))
```
## Frontend Implementation
### Upload Request
```typescript
import { apiClient } from '@/services/api/client';
interface UploadMetadata {
title?: string;
artist?: string;
album?: string;
genre?: string;
year?: number;
description?: string;
is_public?: boolean;
}
async function uploadFile(
file: File,
metadata: UploadMetadata = {},
onProgress?: (progress: number) => void,
): Promise<StandardUploadResponse> {
const formData = new FormData();
formData.append('file', file);
if (metadata.title) formData.append('title', metadata.title);
if (metadata.artist) formData.append('artist', metadata.artist);
if (metadata.album) formData.append('album', metadata.album);
if (metadata.genre) formData.append('genre', metadata.genre);
if (metadata.year) formData.append('year', metadata.year.toString());
if (metadata.description) formData.append('description', metadata.description);
if (metadata.is_public !== undefined) {
formData.append('is_public', metadata.is_public.toString());
}
const response = await apiClient.post<StandardUploadResponse>(
'/tracks',
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
if (progressEvent.total && onProgress) {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total,
);
onProgress(progress);
}
},
},
);
return response.data;
}
```
### Type Definitions
```typescript
interface StandardUploadResponse {
id: string;
track_id?: string;
file_name: string;
file_size: number;
file_type: 'audio' | 'image' | 'video';
mime_type: string;
checksum: string;
status: 'pending' | 'uploading' | 'processing' | 'completed' | 'failed' | 'cancelled';
progress: number;
bytes_uploaded: number;
url: string;
thumbnail_url?: string;
storage_path: string;
storage_provider: string;
is_processed: boolean;
processed_at?: string;
processing_error?: string;
virus_scanned: boolean;
virus_scan_result?: 'clean' | 'infected' | 'error';
virus_scanned_at?: string;
created_at: string;
updated_at: string;
expires_at?: string;
}
```
## Best Practices
### For Backend Developers
1. **Always Use Standard Types**
```go
var req upload.StandardUploadRequest
```
2. **Validate File Size and Type**
```go
if fileSize > maxSize {
return upload.ErrorCodeFileTooLarge
}
```
3. **Use Standard Error Codes**
```go
errorCode := upload.ErrorCodeFileTooLarge
```
4. **Return Standard Response Format**
```go
response := &upload.StandardUploadResponse{...}
RespondSuccess(c, http.StatusCreated, response)
```
### For Frontend Developers
1. **Use Standard Field Names**
```typescript
formData.append('file', file);
formData.append('title', title);
```
2. **Handle Progress Updates**
```typescript
onUploadProgress: (progressEvent) => {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total,
);
onProgress(progress);
}
```
3. **Handle Errors Properly**
```typescript
if (error.response?.data?.error?.code === 'FILE_TOO_LARGE') {
showError('File is too large');
}
```
4. **Validate Before Upload**
```typescript
if (file.size > maxSize) {
throw new Error('File too large');
}
```
## Migration Guide
### Legacy Format (Deprecated)
```go
// Old format
type UploadRequest struct {
TrackID uuid.UUID `form:"track_id"`
FileType string `form:"file_type"`
Title string `form:"title"`
}
```
### Standardized Format
```go
// New format
import "veza-backend-api/internal/upload"
var req upload.StandardUploadRequest
```
## References
- `DATETIME_STANDARD.md` - Date/time format specification
- `ERROR_RESPONSE_STANDARD.md` - Error format specification
- `REQUEST_RESPONSE_VALIDATION_GUIDE.md` - Validation guide
- `veza-backend-api/internal/upload/types.go` - Backend types
- `apps/web/src/types/upload.ts` - Frontend types (to be created)
---
**Last Updated**: 2025-12-25
**Maintained By**: Veza Backend Team