|
| 1 | +# Demo API Documentation |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +The Demo API is a comprehensive system for uploading, storing, and retrieving game demo files with automatic compression. It's designed to handle both tournament and casual game demos with metadata management and efficient file storage using compression. |
| 6 | + |
| 7 | +## Architecture |
| 8 | + |
| 9 | +The Demo API follows a layered architecture pattern: |
| 10 | + |
| 11 | +``` |
| 12 | +HTTP Layer (Gin Router) |
| 13 | + ↓ |
| 14 | +Handler Layer (demo.go) |
| 15 | + ↓ |
| 16 | +Service Layer (demo_service.go) |
| 17 | + ↓ |
| 18 | +Repository Layer (demo_repository.go) |
| 19 | + ↓ |
| 20 | +Storage Layer (MinIO + PostgreSQL) |
| 21 | +``` |
| 22 | + |
| 23 | +### Components |
| 24 | + |
| 25 | +1. **Handler Layer**: HTTP request/response handling and validation |
| 26 | +2. **Service Layer**: Business logic, file compression, and coordination |
| 27 | +3. **Repository Layer**: Database operations using GORM |
| 28 | +4. **Storage Layer**: File storage using MinIO object storage with PostgreSQL metadata |
| 29 | + |
| 30 | +## Data Model |
| 31 | + |
| 32 | +### Demo Entity |
| 33 | + |
| 34 | +```go |
| 35 | +type Demo struct { |
| 36 | + ID int `json:"id"` |
| 37 | + IsTournament bool `json:"is_tournament"` |
| 38 | + TournamentID *int `json:"tournament_id,omitempty"` |
| 39 | + MatchID *int `json:"match_id,omitempty"` |
| 40 | + RoundID *int `json:"round_id,omitempty"` |
| 41 | + RawDemoName string `json:"raw_demo_name"` |
| 42 | + FileSize int64 `json:"file_size"` |
| 43 | + BlueTeamName string `json:"blue_team_name"` |
| 44 | + RedTeamName string `json:"red_team_name"` |
| 45 | + ObjectName string `json:"object_name"` |
| 46 | + ContentType string `json:"content_type"` |
| 47 | + IsCompressed bool `json:"is_compressed"` |
| 48 | + CompressedSize *int64 `json:"compressed_size,omitempty"` |
| 49 | + UploadedAt time.Time `json:"uploaded_at"` |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +### Database Schema |
| 54 | + |
| 55 | +```sql |
| 56 | +CREATE TABLE demos ( |
| 57 | + id SERIAL PRIMARY KEY, |
| 58 | + is_tournament BOOLEAN NOT NULL DEFAULT FALSE, |
| 59 | + tournament_id INTEGER, |
| 60 | + match_id INTEGER, |
| 61 | + round_id INTEGER, |
| 62 | + raw_demo_name VARCHAR(255) NOT NULL, |
| 63 | + file_size BIGINT NOT NULL, |
| 64 | + blue_team_name VARCHAR(100) NOT NULL, |
| 65 | + red_team_name VARCHAR(100) NOT NULL, |
| 66 | + object_name VARCHAR(500) NOT NULL UNIQUE, |
| 67 | + content_type VARCHAR(100), |
| 68 | + is_compressed BOOLEAN DEFAULT TRUE, |
| 69 | + compressed_size BIGINT, |
| 70 | + uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| 71 | + |
| 72 | + CONSTRAINT chk_tournament_data CHECK ( |
| 73 | + (is_tournament = FALSE) OR |
| 74 | + (is_tournament = TRUE AND tournament_id IS NOT NULL AND match_id IS NOT NULL AND round_id IS NOT NULL) |
| 75 | + ) |
| 76 | +); |
| 77 | +``` |
| 78 | + |
| 79 | +### Indexing Strategy |
| 80 | + |
| 81 | +The database includes optimized indexes for common query patterns: |
| 82 | +- `idx_demos_match_round`: For match and round lookups |
| 83 | +- `idx_demos_tournament`: For tournament-specific queries |
| 84 | +- `idx_demos_tournament_match_round`: For combined tournament queries |
| 85 | +- `idx_demos_uploaded_at`: For chronological sorting |
| 86 | +- `idx_demos_object_name`: For file existence checks |
| 87 | + |
| 88 | +## API Endpoints |
| 89 | + |
| 90 | +Base path: `/api/v1/demos` |
| 91 | + |
| 92 | +### 1. Upload Demo |
| 93 | +**POST** `/upload` |
| 94 | + |
| 95 | +Upload a new demo file with automatic compression. |
| 96 | + |
| 97 | +**Authentication**: Requires `secret_password` query parameter |
| 98 | + |
| 99 | +**Request**: `multipart/form-data` |
| 100 | +``` |
| 101 | +demo_file: [file] // Required: Demo file to upload |
| 102 | +is_tournament: boolean // Required: Whether this is a tournament demo |
| 103 | +tournament_id: integer // Required if is_tournament=true |
| 104 | +match_id: integer // Required if is_tournament=true |
| 105 | +round_id: integer // Required if is_tournament=true |
| 106 | +raw_demo_name: string // Required: Original demo name |
| 107 | +blue_team_name: string // Required: Blue team name |
| 108 | +red_team_name: string // Required: Red team name |
| 109 | +``` |
| 110 | + |
| 111 | +**Response**: |
| 112 | +```json |
| 113 | +{ |
| 114 | + "status": "success", |
| 115 | + "message": "Demo uploaded successfully", |
| 116 | + "data": { |
| 117 | + "id": 1, |
| 118 | + "is_tournament": true, |
| 119 | + "tournament_id": 1, |
| 120 | + "match_id": 1, |
| 121 | + "round_id": 1, |
| 122 | + "raw_demo_name": "demo.dem", |
| 123 | + "file_size": 1024000, |
| 124 | + "blue_team_name": "Team Alpha", |
| 125 | + "red_team_name": "Team Beta", |
| 126 | + "object_name": "demos/tournament_1/match_1/round_1/demo_1709123456.dem.zst", |
| 127 | + "content_type": "application/octet-stream", |
| 128 | + "is_compressed": true, |
| 129 | + "compressed_size": 512000, |
| 130 | + "uploaded_at": "2026-02-28T10:00:00Z" |
| 131 | + } |
| 132 | +} |
| 133 | +``` |
| 134 | + |
| 135 | +### 2. Get Demo Information |
| 136 | +**GET** `/` |
| 137 | + |
| 138 | +Retrieve demo information by query parameters. |
| 139 | + |
| 140 | +**Request Parameters**: |
| 141 | +``` |
| 142 | +match_id: integer // Optional: Match ID |
| 143 | +round_id: integer // Optional: Round ID |
| 144 | +tournament_id: integer // Optional: Tournament ID |
| 145 | +``` |
| 146 | + |
| 147 | +**Query Priority**: |
| 148 | +1. tournament_id + match_id + round_id (exact tournament match) |
| 149 | +2. match_id + round_id (match without tournament context) |
| 150 | + |
| 151 | +**Response**: |
| 152 | +```json |
| 153 | +{ |
| 154 | + "status": "success", |
| 155 | + "message": "Demo found", |
| 156 | + "data": { |
| 157 | + // Demo object (same as upload response) |
| 158 | + } |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 162 | +### 3. Download Demo File |
| 163 | +**GET** `/download/:id` |
| 164 | + |
| 165 | +Download the actual demo file by demo ID. The file is automatically decompressed before streaming to the client. |
| 166 | + |
| 167 | +**Response**: Binary file stream with headers: |
| 168 | +``` |
| 169 | +Content-Type: application/octet-stream |
| 170 | +Content-Disposition: attachment; filename="demo.dem" |
| 171 | +Content-Length: [original_file_size] |
| 172 | +``` |
| 173 | + |
| 174 | +**Note**: Files are stored compressed using zstd in the storage layer, but are automatically decompressed when downloaded, so clients receive the original uncompressed demo file. |
| 175 | + |
| 176 | +### 4. List Demos |
| 177 | +**GET** `/list` |
| 178 | + |
| 179 | +Retrieve paginated list of demos. |
| 180 | + |
| 181 | +**Request Parameters**: |
| 182 | +``` |
| 183 | +page: integer // Optional: Page number (default: 1) |
| 184 | +limit: integer // Optional: Items per page (default: 10, max: 100) |
| 185 | +``` |
| 186 | + |
| 187 | +**Response**: |
| 188 | +```json |
| 189 | +{ |
| 190 | + "status": "success", |
| 191 | + "message": "Demos retrieved successfully", |
| 192 | + "data": { |
| 193 | + "demos": [/* Array of demo objects */], |
| 194 | + "page": 1, |
| 195 | + "limit": 10, |
| 196 | + "count": 10 |
| 197 | + } |
| 198 | +} |
| 199 | +``` |
| 200 | + |
| 201 | +### 5. Check Demo Availability |
| 202 | +**HEAD** `/` |
| 203 | + |
| 204 | +Check if a demo exists without returning data (same parameters as GET). |
| 205 | + |
| 206 | +## File Storage & Compression |
| 207 | + |
| 208 | +### Storage Strategy |
| 209 | + |
| 210 | +**Object Storage**: MinIO is used for storing compressed demo files |
| 211 | +- Bucket-based organization |
| 212 | +- Automatic file compression using Zstandard (zstd) |
| 213 | +- Unique object naming prevents conflicts |
| 214 | + |
| 215 | +### Object Naming Convention |
| 216 | + |
| 217 | +**Tournament Demos**: |
| 218 | +``` |
| 219 | +demos/tournament_{tournament_id}/match_{match_id}/round_{round_id}/{filename}_{timestamp}.{ext}.zst |
| 220 | +``` |
| 221 | + |
| 222 | +**Casual Demos**: |
| 223 | +``` |
| 224 | +demos/casual/{filename}_{timestamp}.{ext}.zst |
| 225 | +``` |
| 226 | + |
| 227 | +### Storage Details |
| 228 | + |
| 229 | +- **Algorithm**: Zstandard (zstd) with default speed level |
| 230 | +- **Process**: Files compressed automatically during upload |
| 231 | +- **Storage**: Only compressed versions stored in MinIO for efficiency |
| 232 | +- **Download**: Files automatically decompressed when downloaded to clients |
| 233 | +- **Metadata**: Both original and compressed sizes tracked |
| 234 | +- **Format**: Files stored with `.zst` suffix, served with original names |
| 235 | + |
| 236 | +## Authentication & Security |
| 237 | + |
| 238 | +### Upload Security |
| 239 | +- **Secret Password**: Required for upload operations via query parameter |
| 240 | +- **Middleware**: `SecretPasswordAuth` middleware validates credentials |
| 241 | +- **Public Access**: Read operations (GET, HEAD, LIST, DOWNLOAD) are public |
| 242 | + |
| 243 | +### File Validation |
| 244 | +- **Duplicate Prevention**: Object name uniqueness enforced at database level |
| 245 | +- **Tournament Validation**: Tournament demos require tournament_id, match_id, and round_id |
| 246 | +- **Size Tracking**: Both original and compressed sizes monitored |
| 247 | + |
| 248 | +## Error Handling |
| 249 | + |
| 250 | +### Common HTTP Status Codes |
| 251 | + |
| 252 | +- **200 OK**: Successful operation |
| 253 | +- **400 Bad Request**: Invalid request data or missing required fields |
| 254 | +- **401 Unauthorized**: Missing or invalid secret_password for uploads |
| 255 | +- **404 Not Found**: Demo not found or doesn't exist |
| 256 | +- **500 Internal Server Error**: Server-side errors (database, storage issues) |
| 257 | + |
| 258 | +### Error Response Format |
| 259 | + |
| 260 | +```json |
| 261 | +{ |
| 262 | + "status": "error", |
| 263 | + "message": "Error description" |
| 264 | +} |
| 265 | +``` |
| 266 | + |
| 267 | +## Usage Examples |
| 268 | + |
| 269 | +### Upload Tournament Demo |
| 270 | + |
| 271 | +```bash |
| 272 | +curl -X POST "https://api.udl.tf/v1/demos/upload?secret_password=your_secret" \ |
| 273 | + -F "demo_file=@match1_round1.dem" \ |
| 274 | + -F "is_tournament=true" \ |
| 275 | + -F "tournament_id=1" \ |
| 276 | + -F "match_id=1" \ |
| 277 | + -F "round_id=1" \ |
| 278 | + -F "raw_demo_name=match1_round1.dem" \ |
| 279 | + -F "blue_team_name=Team Alpha" \ |
| 280 | + -F "red_team_name=Team Beta" |
| 281 | +``` |
| 282 | + |
| 283 | +### Get Demo by Tournament Match |
| 284 | + |
| 285 | +```bash |
| 286 | +curl "https://api.udl.tf/v1/demos?tournament_id=1&match_id=1&round_id=1" |
| 287 | +``` |
| 288 | + |
| 289 | +### Download Demo File |
| 290 | + |
| 291 | +```bash |
| 292 | +curl -O "https://api.udl.tf/v1/demos/download/1" |
| 293 | +# Downloads the original demo file (automatically decompressed) |
| 294 | +# File saved as: original_name.dem |
| 295 | +``` |
| 296 | + |
| 297 | +### List Recent Demos |
| 298 | + |
| 299 | +```bash |
| 300 | +curl "https://api.udl.tf/v1/demos/list?page=1&limit=20" |
| 301 | +``` |
| 302 | + |
| 303 | +## Implementation Details |
| 304 | + |
| 305 | +### Service Layer Responsibilities |
| 306 | + |
| 307 | +1. **File Compression**: Automatic zstd compression during upload |
| 308 | +2. **File Decompression**: Automatic decompression during download |
| 309 | +3. **Object Naming**: Generates unique object names with timestamps |
| 310 | +4. **Validation**: Ensures tournament data consistency |
| 311 | +5. **Storage Coordination**: Handles both database and object storage operations |
| 312 | +6. **Cleanup**: Removes storage files if database operations fail |
| 313 | + |
| 314 | +### Repository Layer Operations |
| 315 | + |
| 316 | +- **Create**: Insert new demo records with validation |
| 317 | +- **Read**: Query by ID, tournament context, or match context |
| 318 | +- **List**: Paginated demo retrieval with sorting |
| 319 | +- **Check**: Object name existence verification |
| 320 | +- **Delete**: Record removal (used by service for cleanup) |
| 321 | + |
| 322 | +### Concurrency & Consistency |
| 323 | + |
| 324 | +- **Unique Constraints**: Database enforces object name uniqueness |
| 325 | +- **Transactional Safety**: Database operations fail fast if storage succeeds but DB fails |
| 326 | +- **Cleanup Strategy**: Orphaned storage files cleaned up on DB save failure |
| 327 | + |
| 328 | +## Performance Considerations |
| 329 | + |
| 330 | +### Database Optimization |
| 331 | +- Strategic indexing for common query patterns |
| 332 | +- Offset-based pagination for large datasets |
| 333 | +- Timestamp-based ordering for chronological access |
| 334 | + |
| 335 | +### Storage Optimization |
| 336 | +- Zstd compression reduces storage size and transfer time |
| 337 | +- Object name structure enables efficient bucket organization |
| 338 | +- Presigned URL capability (via storage service) for direct client access |
| 339 | + |
| 340 | +### Scalability Features |
| 341 | +- Stateless service design enables horizontal scaling |
| 342 | +- Separate storage and database layers allow independent scaling |
| 343 | +- Consistent object naming supports load balancing and caching |
0 commit comments