jackspace/claudeskillz/cloudflare-r2

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Complete knowledge domain for Cloudflare R2 - S3-compatible object storage on Cloudflare's edge network. Use when: creating R2 buckets, uploading files to R2, downloading objects, configuring R2 bindings, setting up CORS, generating presigned URLs, multipart uploads, storing images/assets, managing object metadata, or encountering "R2_ERROR", CORS errors, presigned URL failures, multipart upload issues, or storage quota errors. Keywords: r2, r2 storage, cloudflare r2, r2 bucket, r2 upload, r2 download, r2 binding, object storage, s3 compatible, r2 cors, presigned urls, multipart upload, r2 api, r2 workers, file upload, asset storage, R2_ERROR, R2Bucket, r2 metadata, custom metadata, http metadata, content-type, cache-control, aws4fetch, s3 client, bulk delete, r2 list, storage class

Install

npx vskill@latest install jackspace/claudeskillz/cloudflare-r2

SKILL.md

Cloudflare R2 Object Storage

Status: Production Ready ✅ Last Updated: 2025-10-21 Dependencies: cloudflare-worker-base (for Worker setup) Latest Versions: wrangler@4.43.0, @cloudflare/workers-types@4.20251014.0, aws4fetch@1.0.20


Quick Start (5 Minutes)

1. Create R2 Bucket
bash
# Via Wrangler CLI (recommended)
npx wrangler r2 bucket create my-bucket
# Or via Cloudflare Dashboard
# https://dash.cloudflare.com → R2 Object Storage → Create bucket

Bucket Naming Rules:

  • 3-63 characters
  • Lowercase letters, numbers, hyphens only
  • Must start/end with letter or number
  • Globally unique within your account
2. Configure R2 Binding

Add to your wrangler.jsonc:

jsonc
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-11",
"r2_buckets": [
{
"binding": "MY_BUCKET", // Available as env.MY_BUCKET in your Worker
"bucket_name": "my-bucket", // Name from wrangler r2 bucket create
"preview_bucket_name": "my-bucket-preview" // Optional: separate bucket for dev
}
]
}

CRITICAL:

  • binding is how you access the bucket in code (env.MY_BUCKET)
  • bucket_name is the actual R2 bucket name
  • preview_bucket_name is optional but recommended for separate dev/prod data
3. Basic Upload/Download
typescript
// src/index.ts
import { Hono } from 'hono';
type Bindings = {
MY_BUCKET: R2Bucket;
};
const app = new Hono<{ Bindings: Bindings }>();
// Upload file
app.put('/upload/:filename', async (c) => {
const filename = c.req.param('filename');
const body = await c.req.arrayBuffer();
try {
const object = await c.env.MY_BUCKET.put(filename, body, {
httpMetadata: {
contentType: c.req.header('content-type') || 'application/octet-stream',
},
});
return c.json({
success: true,
key: object.key,
size: object.size,
etag: object.etag,
});
} catch (error: any) {
console.error('R2 Upload Error:', error.message);
return c.json({ error: 'Upload failed' }, 500);
}
});
// Download file
app.get('/download/:filename', async (c) => {
const filename = c.req.param('filename');
try {
const object = await c.env.MY_BUCKET.get(filename);
if (!object) {
return c.json({ error: 'File not found' }, 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',
},
});
} catch (error: any) {
console.error('R2 Download Error:', error.message);
return c.json({ error: 'Download failed' }, 500);
}
});
export default app;
4. Deploy and Test
bash
# Deploy
npx wrangler deploy
# Test upload
curl -X PUT https://my-worker.workers.dev/upload/test.txt \
-H "Content-Type: text/plain" \
-d "Hello, R2!"
# Test download
curl https://my-worker.workers.dev/download/test.txt

R2 Workers API

Type Definitions
typescript
// Add to env.d.ts or worker-configuration.d.ts
interface Env {
MY_BUCKET: R2Bucket;
// ... other bindings
}
// For Hono
type Bindings = {
MY_BUCKET: R2Bucket;
};
const app = new Hono<{ Bindings: Bindings }>();
put() - Upload Objects

Signature:

typescript
put(key: string, value: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob, options?: R2PutOptions): Promise<R2Object | null>

Basic Usage:

typescript
// Upload from request body
await env.MY_BUCKET.put('path/to/file.txt', request.body);
// Upload string
await env.MY_BUCKET.put('config.json', JSON.stringify({ foo: 'bar' }));
// Upload ArrayBuffer
await env.MY_BUCKET.put('image.png', await file.arrayBuffer());

With Metadata:

typescript
const object = await env.MY_BUCKET.put('document.pdf', fileData, {
httpMetadata: {
contentType: 'application/pdf',
contentLanguage: 'en-US',
contentDisposition: 'attachment; filename="report.pdf"',
contentEncoding: 'gzip',
cacheControl: 'public, max-age=86400',
},
customMetadata: {
userId: '12345',
uploadDate: new Date().toISOString(),
version: '1.0',
},
});

Conditional Uploads (Prevent Overwrites):

typescript
// Only upload if file doesn't exist
const object = await env.MY_BUCKET.put('file.txt', data, {
onlyIf: {
uploadedBefore: new Date('2020-01-01'), // Any date before R2 existed
},
});
if (!object) {
// File already exists, upload prevented
return c.json({ error: 'File already exists' }, 409);
}
// Only upload if etag matches (update specific version)
const object = await env.MY_BUCKET.put('file.txt', data, {
onlyIf: {
etagMatches: existingEtag,
},
});

With Checksums:

typescript
// R2 will verify the checksum
const md5Hash = await crypto.subtle.digest('MD5', fileData);
await env.MY_BUCKET.put('file.txt', fileData, {
md5: md5Hash,
});
get() - Download Objects

Signature:

typescript
get(key: string, options?: R2GetOptions): Promise<R2ObjectBody | null>

Basic Usage:

typescript
// Get full object
const object = await env.MY_BUCKET.get('file.txt');
if (!object) {
return c.json({ error: 'Not found' }, 404);
}
// Return as response
return new Response(object.body, {
headers: {
'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
'ETag': object.httpEtag,
},
});

Read as Different Formats:

typescript
const object = await env.MY_BUCKET.get('data.json');
if (object) {
const text = await object.text(); // As string
const json = await object.json(); // As JSON object
const buffer = await object.arrayBuffer(); // As ArrayBuffer
const blob = await object.blob(); // As Blob
}

Range Requests (Partial Downloads):

typescript
// Get first 1MB of file
const object = await env.MY_BUCKET.get('large-file.mp4', {
range: { offset: 0, length: 1024 * 1024 },
});
// Get bytes 100-200
const object = await env.MY_BUCKET.get('file.bin', {
range: { offset: 100, length: 100 },
});
// Get from offset to end
const object = await env.MY_BUCKET.get('file.bin', {
range: { offset: 1000 },
});

Conditional Downloads:

typescript
// Only download if etag matches
const object = await env.MY_BUCKET.get('file.txt', {
onlyIf: {
etagMatches: cachedEtag,
},
});
if (!object) {
// Etag didn't match, file was modified
return c.json({ error: 'File changed' }, 412);
}
head() - Get Metadata Only

Signature:

typescript
head(key: string): Promise<R2Object | null>

Usage:

typescript
// Get object metadata without downloading body
const object = await env.MY_BUCKET.head('file.txt');
if (object) {
console.log({
key: object.key,
size: object.size,
etag: object.etag,
uploaded: object.uploaded,
contentType: object.httpMetadata?.contentType,
customMetadata: object.customMetadata,
});
}

Use Cases:

  • Check if file exists
  • Get file size before downloading
  • Check last modified date
  • Validate etag for caching
delete() - Delete Objects

Signature:

typescript
delete(key: string | string[]): Promise<void>

Single Delete:

typescript
// Delete single object
await env.MY_BUCKET.delete('file.txt');
// No error if file doesn't exist (idempotent)

Bulk Delete (Up to 1000 keys):

typescript
// Delete multiple objects at once
const keysToDelete = [
'old-file-1.txt',
'old-file-2.txt',
'temp/cache-data.json',
];
await env.MY_BUCKET.delete(keysToDelete);
// Much faster than individual deletes

Delete with Confirmation:

typescript
app.delete('/files/:filename', async (c) => {
const filename = c.req.param('filename');
// Check if exists first
const exists = await c.env.MY_BUCKET.head(filename);
if (!exists) {
return c.json({ error: 'File not found' }, 404);
}
await c.env.MY_BUCKET.delete(filename);
return c.json({ success: true, deleted: filename });
});
list() - List Objects

Signature:

typescript
list(options?: R2ListOptions): Promise<R2Objects>

Basic Listing:

typescript
// List all objects (up to 1000)
const listed = await env.MY_BUCKET.list();
console.log({
objects: listed.objects, // Array of R2Object
truncated: listed.truncated, // true if more results exist
cursor: listed.cursor, // For pagination
});
// Process objects
for (const object of listed.objects) {
console.log(`${object.key}: ${object.size} bytes`);
}

Pagination:

typescript
app.get('/api/files', async (c) => {
const cursor = c.req.query('cursor');
const listed = await c.env.MY_BUCKET.list({
limit: 100,
cursor: cursor || undefined,
});
return c.json({
files: listed.objects.map(obj => ({
name: obj.key,
size: obj.size,
uploaded: obj.uploaded,
etag: obj.etag,
})),
hasMore: listed.truncated,
nextCursor: listed.cursor,
});
});

Prefix Filtering (List by Directory):

typescript
// List all files in 'images/' folder
const images = await env.MY_BUCKET.list({
prefix: 'images/',
});
// List all user files
const userFiles = await env.MY_BUCKET.list({
prefix: `users/${userId}/`,
});

Delimiter (Folder-like Listing):

typescript
// List only top-level items in 'uploads/'
const listed = await env.MY_BUCKET.list({
prefix: 'uploads/',
delimiter: '/',
});
console.log('Files:', listed.objects); // Files directly in uploads/
console.log('Folders:', listed.delimitedPrefixes); // Sub-folders like uploads/2024/

Multipart Uploads

For files larger than 100MB or for resumable uploads, use multipart upload API.

When to Use Multipart Upload

✅ Use multipart when:

  • File size > 100MB
  • Need resumable uploads
  • Want to parallelize upload
  • Uploading from browser/client

❌ Don't use multipart when:

  • File size < 5MB (overhead not worth it)
  • Uploading within Worker (use direct put())
Basic Multipart Upload Flow
typescript
app.post('/api/upload/start', async (c) => {
const { filename } = await c.req.json();
// Create multipart upload
const multipart = await c.env.MY_BUCKET.createMultipartUpload(filename, {
httpMetadata: {
contentType: 'application/octet-stream',
},
});
return c.json({
key: multipart.key,
uploadId: multipart.uploadId,
});
});
app.put('/api/upload/part', async (c) => {
const { key, uploadId, partNumber } = await c.req.json();
const body = await c.req.arrayBuffer();
// Resume the multipart upload
const multipart = c.env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
// Upload a part
const uploadedPart = await multipart.uploadPart(partNumber, body);
return c.json({
partNumber: uploadedPart.partNumber,
etag: uploadedPart.etag,
});
});
app.post('/api/upload/complete', async (c) => {
const { key, uploadId, parts } = await c.req.json();
// Resume the multipart upload
const multipart = c.env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
// Complete the upload
const object = await multipart.complete(parts);
return c.json({
success: true,
key: object.key,
size: object.size,
etag: object.etag,
});
});
app.post('/api/upload/abort', async (c) => {
const { key, uploadId } = await c.req.json();
const multipart = c.env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
await multipart.abort();
return c.json({ success: true });
});
Complete Multipart Upload Example
typescript
// Full Worker implementing multipart upload API
interface Env {
MY_BUCKET: R2Bucket;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const key = url.pathname.slice(1);
const action = url.searchParams.get('action');

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
14
Repository forks
5
7-day trend score
14

Stars and forks are for the whole GitHub repository, not this skill alone. Refreshed 15 Apr 2026, 6 months ago.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/jackspace/claudeskillz/cloudflare-r2/badge)](https://verified-skill.com/skills/jackspace/claudeskillz/cloudflare-r2)

Something wrong with this skill or its page? Tell us.