Skip to content

Commit a6698a5

Browse files
committed
feat: Add comprehensive documentation for Demo API and enhance download functionality
1 parent ff014e0 commit a6698a5

4 files changed

Lines changed: 423 additions & 7 deletions

File tree

docs/demos-api-quick-reference.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Demo API Quick Reference
2+
3+
## Endpoints Overview
4+
5+
| Method | Endpoint | Purpose | Auth Required |
6+
| ------ | ------------------------ | ------------------ | ----------------- |
7+
| POST | `/v1/demos/upload` | Upload demo file | ✅ Secret password |
8+
| GET | `/v1/demos` | Get demo info | ❌ Public |
9+
| GET | `/v1/demos/download/:id` | Download file | ❌ Public |
10+
| GET | `/v1/demos/list` | List demos | ❌ Public |
11+
| HEAD | `/v1/demos` | Check availability | ❌ Public |
12+
13+
## Key Features
14+
15+
-**Automatic Compression**: Files compressed with Zstd algorithm for storage
16+
- 📤 **Smart Downloads**: Files automatically decompressed when downloaded
17+
- 🎮 **Tournament Support**: Organized by tournament/match/round structure
18+
- 📁 **Smart Storage**: MinIO object storage with PostgreSQL metadata
19+
- 🔍 **Flexible Queries**: Search by match, round, or tournament context
20+
- 📄 **Pagination**: Efficient listing with page/limit support
21+
22+
## Quick Examples
23+
24+
### Upload Demo
25+
```bash
26+
curl -X POST "https://api.udl.tf/v1/demos/upload?secret_password=secret" \
27+
-F "demo_file=@demo.dem" \
28+
-F "is_tournament=true" \
29+
-F "tournament_id=1" \
30+
-F "match_id=1" \
31+
-F "round_id=1" \
32+
-F "raw_demo_name=demo.dem" \
33+
-F "blue_team_name=Team A" \
34+
-F "red_team_name=Team B"
35+
```
36+
37+
### Get Demo Info
38+
```bash
39+
curl "https://api.udl.tf/v1/demos?match_id=1&round_id=1"
40+
```
41+
42+
### Download Demo
43+
```bash
44+
curl -O "https://api.udl.tf/v1/demos/download/1"
45+
# Downloads original demo file (automatically decompressed)
46+
```
47+
48+
For detailed documentation, see [demos-api.md](./demos-api.md).

docs/demos-api.md

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
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

Comments
 (0)