Ga naar hoofdinhoud

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

  1. Ga naar Cloudflare Dashboard
  2. R2 → Create bucket
  3. Naam: milenas-treehouse-assets
  4. Location hint: Europe (dichtstbij)

2. API Tokens

  1. R2 → Manage R2 API Tokens
  2. Create API token
  3. Permissions: Object Read & Write
  4. Noteer:
    • Account ID
    • Access Key ID
    • Secret Access Key

3. Public Access (voor game assets)

  1. Bucket → Settings → Public access
  2. Enable: assets.yourdomain.com
  3. 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

  1. assets.yourdomain.com/assets/*

    • Cache Level: Cache Everything
    • Edge Cache TTL: 1 month
  2. 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

ResourceGratis
Storage10 GB
Class A ops1M/maand
Class B ops10M/maand
EgressOnbeperkt

Verwacht Gebruik

Asset TypeSizeAantalTotaal
Character sprites50KB502.5MB
Item icons10KB2002MB
Backgrounds500KB2010MB
UI elements20KB1002MB
Totaal~17MB

Ruim binnen free tier!

Volgende