Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Cloudflare platform skill covering Workers, D1, R2, KV, AI, Durable Objects, and security.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/r2/patterns.md
1# R2 Patterns & Best Practices23## Streaming Large Files45```typescript6const object = await env.MY_BUCKET.get(key);7if (!object) return new Response('Not found', { status: 404 });89const headers = new Headers();10object.writeHttpMetadata(headers);11headers.set('etag', object.httpEtag);1213return new Response(object.body, { headers });14```1516## Conditional GET (304 Not Modified)1718```typescript19const ifNoneMatch = request.headers.get('if-none-match');20const object = await env.MY_BUCKET.get(key, {21onlyIf: { etagDoesNotMatch: ifNoneMatch?.replace(/"/g, '') || '' }22});2324if (!object) return new Response('Not found', { status: 404 });25if (!object.body) return new Response(null, { status: 304, headers: { 'etag': object.httpEtag } });2627return new Response(object.body, { headers: { 'etag': object.httpEtag } });28```2930## Upload with Validation3132```typescript33const key = url.pathname.slice(1);34if (!key || key.includes('..')) return new Response('Invalid key', { status: 400 });3536const object = await env.MY_BUCKET.put(key, request.body, {37httpMetadata: { contentType: request.headers.get('content-type') || 'application/octet-stream' },38customMetadata: { uploadedAt: new Date().toISOString(), ip: request.headers.get('cf-connecting-ip') || 'unknown' }39});4041return Response.json({ key: object.key, size: object.size, etag: object.httpEtag });42```4344## Multipart with Progress4546```typescript47const PART_SIZE = 5 * 1024 * 1024; // 5MB48const partCount = Math.ceil(file.size / PART_SIZE);49const multipart = await env.MY_BUCKET.createMultipartUpload(key, { httpMetadata: { contentType: file.type } });5051const uploadedParts: R2UploadedPart[] = [];52try {53for (let i = 0; i < partCount; i++) {54const start = i * PART_SIZE;55const part = await multipart.uploadPart(i + 1, file.slice(start, start + PART_SIZE));56uploadedParts.push(part);57onProgress?.(Math.round(((i + 1) / partCount) * 100));58}59return await multipart.complete(uploadedParts);60} catch (error) {61await multipart.abort();62throw error;63}64```6566## Batch Delete6768```typescript69async function deletePrefix(prefix: string, env: Env) {70let cursor: string | undefined;71let truncated = true;7273while (truncated) {74const listed = await env.MY_BUCKET.list({ prefix, limit: 1000, cursor });75if (listed.objects.length > 0) {76await env.MY_BUCKET.delete(listed.objects.map(o => o.key));77}78truncated = listed.truncated;79cursor = listed.cursor;80}81}82```8384## Checksum Validation & Storage Transitions8586```typescript87// Upload with checksum88const hash = await crypto.subtle.digest('SHA-256', data);89await env.MY_BUCKET.put(key, data, { sha256: hash });9091// Transition storage class (requires S3 SDK)92import { S3Client, CopyObjectCommand } from '@aws-sdk/client-s3';93await s3.send(new CopyObjectCommand({94Bucket: 'my-bucket', Key: key,95CopySource: `/my-bucket/${key}`,96StorageClass: 'STANDARD_IA'97}));98```99100## Client-Side Uploads (Presigned URLs)101102```typescript103import { S3Client } from '@aws-sdk/client-s3';104import { getSignedUrl } from '@aws-sdk/s3-request-presigner';105import { PutObjectCommand } from '@aws-sdk/client-s3';106107// Worker: Generate presigned upload URL108const s3 = new S3Client({109region: 'auto',110endpoint: `https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com`,111credentials: { accessKeyId: env.R2_ACCESS_KEY_ID, secretAccessKey: env.R2_SECRET_ACCESS_KEY }112});113114const url = await getSignedUrl(s3, new PutObjectCommand({ Bucket: 'my-bucket', Key: key }), { expiresIn: 3600 });115return Response.json({ uploadUrl: url });116117// Client: Upload directly118const { uploadUrl } = await fetch('/api/upload-url').then(r => r.json());119await fetch(uploadUrl, { method: 'PUT', body: file });120```121122## Caching with Cache API123124```typescript125export default {126async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {127const cache = caches.default;128const url = new URL(request.url);129const cacheKey = new Request(url.toString(), request);130131// Check cache first132let response = await cache.match(cacheKey);133if (response) return response;134135// Fetch from R2136const key = url.pathname.slice(1);137const object = await env.MY_BUCKET.get(key);138if (!object) return new Response('Not found', { status: 404 });139140const headers = new Headers();141object.writeHttpMetadata(headers);142headers.set('etag', object.httpEtag);143headers.set('cache-control', 'public, max-age=31536000, immutable');144145response = new Response(object.body, { headers });146147// Cache for subsequent requests148ctx.waitUntil(cache.put(cacheKey, response.clone()));149150return response;151}152};153```154155## Public Bucket with Custom Domain156157```typescript158export default {159async fetch(request: Request, env: Env): Promise<Response> {160// CORS preflight161if (request.method === 'OPTIONS') {162return new Response(null, {163headers: {164'access-control-allow-origin': '*',165'access-control-allow-methods': 'GET, HEAD',166'access-control-max-age': '86400'167}168});169}170171const key = new URL(request.url).pathname.slice(1);172if (!key) return Response.redirect('/index.html', 302);173174const object = await env.MY_BUCKET.get(key);175if (!object) return new Response('Not found', { status: 404 });176177const headers = new Headers();178object.writeHttpMetadata(headers);179headers.set('etag', object.httpEtag);180headers.set('access-control-allow-origin', '*');181headers.set('cache-control', 'public, max-age=31536000, immutable');182183return new Response(object.body, { headers });184}185};186```187188## r2.dev Public URLs189190Enable r2.dev in dashboard for simple public access: `https://pub-${hashId}.r2.dev/${key}`191Or add custom domain via dashboard: `https://files.example.com/${key}`192193**Limitations:** No auth, bucket-level CORS, no cache override.194