r2-storage
S3-compatible object storage for files, images, and large data. Load when handling file uploads, storing images/videos/documents, generating presigned URLs, using multipart uploads for large files, migrating from S3, or serving static assets from buckets.
What this skill does
# R2 Object Storage
Store and retrieve objects at scale using Cloudflare's S3-compatible object storage.
## When to Use
- File uploads (images, videos, documents)
- AI assets and model artifacts
- Image and media asset storage
- Structured data (JSON, CSV, logs)
- User-facing uploads and downloads
- Static asset hosting
- Backup and archival storage
- S3-compatible workflows with existing tools
## FIRST: Create R2 Bucket
```bash
# Create bucket
wrangler r2 bucket create my-bucket
# Create with location hint
wrangler r2 bucket create my-bucket --location wnam
# List buckets
wrangler r2 bucket list
```
## Quick Reference
| Operation | API |
|-----------|-----|
| Upload object | `await bucket.put(key, data, { httpMetadata })` |
| Download object | `const obj = await bucket.get(key); const data = await obj.text()` |
| Delete object | `await bucket.delete(key)` |
| List objects | `const list = await bucket.list({ prefix, limit })` |
| Get metadata | `const obj = await bucket.head(key)` |
| Multipart upload | `const upload = await bucket.createMultipartUpload(key)` |
| Generate signed URL | Use presigned URL patterns with R2's S3 compatibility |
## Wrangler Configuration
```jsonc
// wrangler.jsonc
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2026-01-01",
"r2_buckets": [
{
"binding": "BUCKET",
"bucket_name": "my-bucket"
}
]
}
```
**TypeScript Types** (run `wrangler types` to generate):
```typescript
export interface Env {
BUCKET: R2Bucket;
}
```
## Basic Upload and Download
```typescript
import { R2Bucket } from "@cloudflare/workers-types";
export interface Env {
BUCKET: R2Bucket;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const key = url.pathname.slice(1); // Remove leading /
// Upload
if (request.method === "PUT") {
await env.BUCKET.put(key, request.body, {
httpMetadata: {
contentType: request.headers.get("content-type") || "application/octet-stream",
},
});
return new Response("Uploaded", { status: 201 });
}
// Download
if (request.method === "GET") {
const object = await env.BUCKET.get(key);
if (!object) {
return new Response("Not found", { status: 404 });
}
return new Response(object.body, {
headers: {
"Content-Type": object.httpMetadata?.contentType || "application/octet-stream",
"ETag": object.httpEtag,
"Cache-Control": object.httpMetadata?.cacheControl || "public, max-age=3600",
},
});
}
// Delete
if (request.method === "DELETE") {
await env.BUCKET.delete(key);
return new Response("Deleted", { status: 204 });
}
return new Response("Method not allowed", { status: 405 });
},
};
```
## Multipart Form Upload Handler
Handle file uploads from HTML forms or multipart requests.
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const formData = await request.formData();
const file = formData.get("file") as File;
if (!file) {
return new Response("No file provided", { status: 400 });
}
// Generate unique key
const key = `uploads/${crypto.randomUUID()}-${file.name}`;
// Upload to R2
await env.BUCKET.put(key, file.stream(), {
httpMetadata: {
contentType: file.type,
},
customMetadata: {
originalName: file.name,
uploadedAt: new Date().toISOString(),
},
});
return Response.json({
success: true,
key,
url: `/files/${key}`,
});
},
};
```
## List Objects with Pagination
```typescript
async function listAllObjects(
bucket: R2Bucket,
prefix: string = ""
): Promise<R2Object[]> {
const objects: R2Object[] = [];
let cursor: string | undefined;
do {
const listed = await bucket.list({
prefix,
cursor,
limit: 1000,
});
objects.push(...listed.objects);
cursor = listed.truncated ? listed.cursor : undefined;
} while (cursor);
return objects;
}
// Usage
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const prefix = url.searchParams.get("prefix") || "";
const objects = await listAllObjects(env.BUCKET, prefix);
return Response.json({
count: objects.length,
objects: objects.map((obj) => ({
key: obj.key,
size: obj.size,
uploaded: obj.uploaded,
})),
});
},
};
```
## Conditional Operations (ETags)
Use ETags for conditional reads/writes to prevent race conditions.
```typescript
// Conditional write (only if not modified)
const existingObject = await env.BUCKET.head("config.json");
if (existingObject) {
// Update only if ETag matches
await env.BUCKET.put("config.json", newData, {
httpMetadata: {
contentType: "application/json",
},
onlyIf: {
etagMatches: existingObject.httpEtag,
},
});
}
// Conditional read (If-None-Match)
const object = await env.BUCKET.get("image.jpg", {
onlyIf: {
etagDoesNotMatch: cachedEtag,
},
});
if (object === null) {
// Object not modified - return 304
return new Response(null, {
status: 304,
headers: { "ETag": cachedEtag },
});
}
```
## Custom Metadata
Store application-specific metadata alongside objects.
```typescript
// Store with custom metadata
await env.BUCKET.put("document.pdf", pdfData, {
httpMetadata: {
contentType: "application/pdf",
},
customMetadata: {
userId: "user-123",
documentType: "invoice",
version: "2",
tags: "finance,2024",
},
});
// Read metadata without downloading body
const object = await env.BUCKET.head("document.pdf");
console.log(object.customMetadata?.userId); // "user-123"
```
## Range Requests (Partial Downloads)
Efficiently download portions of large files.
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const key = new URL(request.url).pathname.slice(1);
const rangeHeader = request.headers.get("range");
if (rangeHeader) {
// Parse range: "bytes=0-1023"
const match = rangeHeader.match(/bytes=(\d+)-(\d*)/);
if (match) {
const start = parseInt(match[1], 10);
const end = match[2] ? parseInt(match[2], 10) : undefined;
const object = await env.BUCKET.get(key, {
range: { offset: start, length: end ? end - start + 1 : undefined },
});
if (!object) {
return new Response("Not found", { status: 404 });
}
return new Response(object.body, {
status: 206,
headers: {
"Content-Type": object.httpMetadata?.contentType || "application/octet-stream",
"Content-Range": `bytes ${start}-${end || object.size - 1}/${object.size}`,
"Content-Length": object.size.toString(),
},
});
}
}
// Regular full download
const object = await env.BUCKET.get(key);
if (!object) {
return new Response("Not found", { status: 404 });
}
return new Response(object.body, {
headers: {
"Content-Type": object.httpMetadata?.contentType || "application/octet-stream",
},
});
},
};
```
## AWS SDK Integration (S3 Compatible)
R2 is fully compatible with the AWS S3 API. Use the official AWS SDK v3.
**Install dependencies:**
```bash
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
```
**wrangler.jsonc** (add Node.js compatibility):
```jsonc
{
"compatibility_flags": ["nodejs_compat_v2"],
"r2_buckets": [
{ "binding": "BUCKET", "bucket_name": "my-bucket" }
]
}
```
**Using AWS SDK:**
```typescript
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
impRelated in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.