Cloudflare R2
Object storage voor game assets en uploads.
Waarom R2?
- Geen egress kosten - Gratis bandwidth
- S3 compatible - Bestaande tools werken
- Global CDN - Snel overal ter wereld
- Generous free tier - 10GB storage, 1M requests/maand
Setup
1. R2 Bucket Aanmaken
- Ga naar Cloudflare Dashboard
- R2 → Create bucket
- Naam:
milenas-treehouse-assets - Location hint: Europe (dichtstbij)
2. API Tokens
- R2 → Manage R2 API Tokens
- Create API token
- Permissions: Object Read & Write
- Noteer:
- Account ID
- Access Key ID
- Secret Access Key
3. Public Access (voor game assets)
- Bucket → Settings → Public access
- Enable:
assets.yourdomain.com - Of: R2.dev subdomain
Folder Structuur
milenas-treehouse-assets/
├── assets/ # Game assets (public)
│ ├── characters/
│ ├── items/
│ ├── ui/
│ └── maps/
├── tickets/ # Asset pipeline uploads
│ └── {ticket_id}/
│ ├── v1/
│ └── v2/
└── backups/ # Database backups (private)
Integratie
Environment Variables
# .env
R2_ACCOUNT_ID=your_account_id
R2_ACCESS_KEY=your_access_key
R2_SECRET_KEY=your_secret_key
R2_BUCKET=milenas-treehouse-assets
R2_PUBLIC_URL=https://assets.yourdomain.com
TypeScript SDK
// lib/r2.ts
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
const r2 = new S3Client({
region: 'auto',
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY!,
secretAccessKey: process.env.R2_SECRET_KEY!,
},
});
export async function uploadFile(
key: string,
body: Buffer,
contentType: string
): Promise<string> {
await r2.send(new PutObjectCommand({
Bucket: process.env.R2_BUCKET,
Key: key,
Body: body,
ContentType: contentType,
}));
return `${process.env.R2_PUBLIC_URL}/${key}`;
}
export async function deleteFile(key: string): Promise<void> {
await r2.send(new DeleteObjectCommand({
Bucket: process.env.R2_BUCKET,
Key: key,
}));
}
Asset Upload Voorbeeld
// api/assets/upload.ts
import { uploadFile } from '@/lib/r2';
import { getImageDimensions } from '@/lib/image';
export async function POST(req: Request) {
const formData = await req.formData();
const file = formData.get('file') as File;
const ticketId = formData.get('ticketId') as string;
const version = formData.get('version') as string;
// Validate file
if (!file.type.startsWith('image/')) {
return Response.json({ error: 'Must be an image' }, { status: 400 });
}
if (file.size > 5 * 1024 * 1024) {
return Response.json({ error: 'Max 5MB' }, { status: 400 });
}
// Upload to R2
const buffer = Buffer.from(await file.arrayBuffer());
const key = `tickets/${ticketId}/v${version}/${file.name}`;
const url = await uploadFile(key, buffer, file.type);
// Get dimensions
const dimensions = await getImageDimensions(buffer);
return Response.json({
url,
width: dimensions.width,
height: dimensions.height,
size: file.size,
});
}
Godot Integratie
Asset Loading
# assets/remote_loader.gd
extends Node
const R2_BASE_URL = "https://assets.yourdomain.com"
var _cache: Dictionary = {}
func load_remote_texture(path: String) -> Texture2D:
var url = R2_BASE_URL + "/" + path
# Check cache first
if _cache.has(path):
return _cache[path]
# Download
var http = HTTPRequest.new()
add_child(http)
http.request(url)
var result = await http.request_completed
if result[0] != HTTPRequest.RESULT_SUCCESS:
push_error("Failed to load: " + url)
return null
# Create texture
var image = Image.new()
image.load_png_from_buffer(result[3])
var texture = ImageTexture.create_from_image(image)
# Cache it
_cache[path] = texture
http.queue_free()
return texture
Preloading Assets
func preload_assets(paths: Array[String]) -> void:
for path in paths:
load_remote_texture(path)
Cache Headers
Cloudflare Page Rules
-
assets.yourdomain.com/assets/*- Cache Level: Cache Everything
- Edge Cache TTL: 1 month
-
assets.yourdomain.com/tickets/*- Cache Level: Cache Everything
- Edge Cache TTL: 1 week
R2 Object Metadata
await r2.send(new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: body,
ContentType: contentType,
CacheControl: 'public, max-age=2592000', // 30 dagen
}));
Backup Upload
// lib/backup.ts
export async function uploadBackup(data: object): Promise<string> {
const date = new Date().toISOString().split('T')[0];
const key = `backups/${date}/database.json`;
await uploadFile(
key,
Buffer.from(JSON.stringify(data)),
'application/json'
);
return key;
}
Kosten Berekening
Free Tier
| Resource | Gratis |
|---|---|
| Storage | 10 GB |
| Class A ops | 1M/maand |
| Class B ops | 10M/maand |
| Egress | Onbeperkt |
Verwacht Gebruik
| Asset Type | Size | Aantal | Totaal |
|---|---|---|---|
| Character sprites | 50KB | 50 | 2.5MB |
| Item icons | 10KB | 200 | 2MB |
| Backgrounds | 500KB | 20 | 10MB |
| UI elements | 20KB | 100 | 2MB |
| Totaal | ~17MB |
Ruim binnen free tier!