File Uploads
File upload patterns in ZoneVast. Covers the presigned URL flow, direct upload, CloudFront serving, and the attachment model.
File Uploads
All file uploads in ZoneVast go through the Project Service (zv-project-service). Files are stored in S3 and served via CloudFront. The service supports two upload methods: presigned URL (recommended for production) and direct upload (for testing).
Presigned URL Flow (Recommended)
The presigned URL flow uploads files directly to S3, bypassing the API Gateway's payload size limit.
Step 1: Request a Presigned URL
POST /api/v1/project/project/attachment/presigned-url/
Authorization: Bearer <token>
X-Project-ID: <project-id>
Content-Type: application/json
Body:
{
"file_name": "product-image.jpg",
"content_type": "image/jpeg",
"file_size": 1048576,
"base_path": "products/images",
"entity_type": "product"
}
Response:
{
"attachment_id": 42,
"upload_url": "https://s3.eu-central-1.amazonaws.com/file-zonevast-eu/...",
"fields": {
"key": "products/images/uuid-product-image.jpg",
"policy": "base64-encoded-policy",
"x-amz-credential": "...",
"x-amz-signature": "..."
},
"s3_key": "products/images/uuid-product-image.jpg"
}
Step 2: Upload to S3
Upload the file directly to the presigned URL:
curl -X POST "https://s3.eu-central-1.amazonaws.com/file-zonevast-eu/..." \
-F "key=products/images/uuid-product-image.jpg" \
-F "policy=base64-encoded-policy" \
-F "x-amz-credential=..." \
-F "x-amz-signature=..." \
-F "file=@/path/to/product-image.jpg"
Or in TypeScript:
async function uploadToPresignedUrl(
file: File,
uploadUrl: string,
fields: Record<string, string>
): Promise<void> {
const formData = new FormData();
Object.entries(fields).forEach(([key, value]) => {
formData.append(key, value);
});
formData.append('file', file);
const response = await fetch(uploadUrl, {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
}
Step 3: Confirm Upload
POST /api/v1/project/project/attachment/confirm-upload/
Authorization: Bearer <token>
X-Project-ID: <project-id>
Content-Type: application/json
Body:
{
"attachment_id": 42
}
Response:
{
"id": 42,
"original_name": "product-image.jpg",
"file_name": "uuid-product-image.jpg",
"type": "image",
"mime_type": "image/jpeg",
"size": 1048576,
"s3_key": "products/images/uuid-product-image.jpg",
"upload_status": "completed",
"url": "https://file.zonevast.com/products/images/uuid-product-image.jpg"
}
Direct Upload (Testing Only)
For quick testing, you can upload directly through the API. This method has a ~10MB limit due to API Gateway payload restrictions.
POST /api/v1/project/project/attachment/direct-upload/
Authorization: Bearer <token>
X-Project-ID: <project-id>
Content-Type: multipart/form-data
Body: multipart form with a file field.
curl -X POST https://test.zonevast.com/api/v1/project/project/attachment/direct-upload/ \
-H "Authorization: Bearer <token>" \
-H "X-Project-ID: <project-id>" \
-F "file=@/path/to/image.jpg" \
-F "base_path=products" \
-F "entity_type=product"
Warning: Direct upload sends the file through API Gateway → Lambda → Django → S3. This is slower and limited to ~10MB. Use presigned URLs for production.
File URL Format
After upload, files are served via CloudFront:
https://file.zonevast.com/{s3_key}
Example:
https://file.zonevast.com/products/images/uuid-product-image.jpg
CloudFront URLs are publicly accessible — no authentication needed to view uploaded files.
Supported File Types
The type field is automatically determined from the MIME type:
|| Type | MIME Examples |
||------|--------------|
|| image | image/jpeg, image/png, image/gif, image/webp |
|| video | video/mp4, video/webm |
|| audio | audio/mpeg, audio/ogg |
|| document | application/pdf, application/msword |
|| other | Any other MIME type |
Deduplication
The upload system supports file deduplication via SHA-256 hash. Include file_hash in your presigned URL request:
{
"file_name": "image.jpg",
"content_type": "image/jpeg",
"file_size": 1048576,
"base_path": "products",
"entity_type": "product",
"file_hash": "sha256-hash-of-file-contents"
}
If a file with the same hash already exists in the same project and base path, the existing attachment is returned instead of creating a duplicate.
Complete Upload Example
async function uploadFile(
file: File,
projectId: string,
token: string
): Promise<{ url: string; attachmentId: number }> {
const baseUrl = 'https://test.zonevast.com/api/v1/project/project';
const headers = {
'Authorization': `Bearer ${token}`,
'X-Project-ID': projectId,
'Content-Type': 'application/json',
};
// Step 1: Get presigned URL
const presignRes = await fetch(`${baseUrl}/attachment/presigned-url/`, {
method: 'POST',
headers,
body: JSON.stringify({
file_name: file.name,
content_type: file.type,
file_size: file.size,
base_path: 'uploads',
entity_type: 'general',
}),
});
const { attachment_id, upload_url, fields } = await presignRes.json();
// Step 2: Upload to S3
await uploadToPresignedUrl(file, upload_url, fields);
// Step 3: Confirm upload
const confirmRes = await fetch(`${baseUrl}/attachment/confirm-upload/`, {
method: 'POST',
headers,
body: JSON.stringify({ attachment_id }),
});
const attachment = await confirmRes.json();
return {
url: `https://file.zonevast.com/${attachment.s3_key}`,
attachmentId: attachment.id,
};
}
Attachment Model
|| Field | Type | Description |
||-------|------|-------------|
|| id | number | Unique identifier |
|| original_name | string | Original file name from upload |
|| file_name | string | Stored file name (UUID-based) |
|| type | string | File category (image, video, audio, document, other) |
|| mime_type | string | Full MIME type |
|| size | number | File size in bytes |
|| s3_key | string | S3 object key |
|| base_path | string | Logical folder in S3 |
|| upload_status | string | pending, completed, or failed |
|| hash | string | SHA-256 hash for deduplication |
|| entity_type | string | Business entity this file belongs to |
|| tenant_project_id | number | Project that owns this file |
What's Next
- Service Integration — How services communicate
- API Response Format — Response structure
- Project Service — Full Project Service API reference