# 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 { 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( '/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