---
name: "Cloudflare R2 Storage"
description: "Skill for Cloudflare R2 Storage — auto-generated from documentation"
version: "1.0.0"
author: "skynet"
category: "infrastructure"
agents: ["claude-code", "codex", "gemini"]
tags: ["cloudflare-r2", "infrastructure", "auto-generated"]
---

# Cloudflare R2 Storage

---
name: Cloudflare R2 Storage
description: Use this skill when you need to work with Cloudflare R2 object storage - creating buckets, uploading/downloading files, managing access policies, configuring CDN integration, or troubleshooting storage operations
metadata:
  author: skynet
  version: 1.0.0
category: infrastructure
---

# Cloudflare R2 Storage

## Overview
Cloudflare R2 is S3-compatible object storage with zero egress fees. Use R2 for file storage, backup, static site hosting, and CDN origin.

## Prerequisites
```bash
# Install Wrangler CLI
npm install -g wrangler

# Authenticate with Cloudflare
wrangler login

# Verify authentication
wrangler whoami
```

## Core Operations

### Bucket Management
```bash
# Create bucket
wrangler r2 bucket create my-bucket

# List buckets
wrangler r2 bucket list

# Delete bucket (must be empty)
wrangler r2 bucket delete my-bucket
```

### File Operations
```bash
# Upload file
wrangler r2 object put my-bucket/path/file.txt --file ./local-file.txt

# Upload with metadata
wrangler r2 object put my-bucket/file.txt --file ./file.txt \
  --content-type "text/plain" \
  --cache-control "max-age=3600"

# Download file
wrangler r2 object get my-bucket/file.txt --file ./downloaded-file.txt

# List objects
wrangler r2 object list my-bucket

# List with prefix
wrangler r2 object list my-bucket --prefix "images/"

# Delete object
wrangler r2 object delete my-bucket/file.txt
```

## Access Control

### R2 Token Creation
```bash
# Create API token with R2 permissions
# Go to Cloudflare Dashboard > My Profile > API Tokens
# Use "Edit Cloudflare Workers" template + R2:Edit permissions
```

### Bucket Policies
```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-bucket/public/*"
    }
  ]
}
```

### Apply bucket policy
```bash
# Save policy as policy.json
wrangler r2 bucket policy put my-bucket --file policy.json

# View current policy
wrangler r2 bucket policy get my-bucket

# Delete policy
wrangler r2 bucket policy delete my-bucket
```

## S3 API Compatibility

### Configure AWS CLI
```bash
# Configure AWS CLI for R2
aws configure set aws_access_key_id YOUR_R2_ACCESS_KEY
aws configure set aws_secret_access_key YOUR_R2_SECRET_KEY
aws configure set region auto

# Set R2 endpoint
export AWS_ENDPOINT_URL=https://ACCOUNT_ID.r2.cloudflarestorage.com
```

### S3 Commands
```bash
# List buckets
aws s3 ls --endpoint-url=$AWS_ENDPOINT_URL

# Sync directory
aws s3 sync ./local-folder s3://my-bucket/folder/ --endpoint-url=$AWS_ENDPOINT_URL

# Copy with public read
aws s3 cp file.txt s3://my-bucket/ --acl public-read --endpoint-url=$AWS_ENDPOINT_URL

# Generate presigned URL
aws s3 presign s3://my-bucket/file.txt --expires-in 3600 --endpoint-url=$AWS_ENDPOINT_URL
```

## CDN Integration

### Custom Domain Setup
```bash
# Add custom domain to R2 bucket
# In Cloudflare Dashboard: R2 > Bucket > Settings > Public access
# Connect custom domain: files.example.com

# Update DNS (automatically done by Cloudflare)
# CNAME files -> bucket-name.account-id.r2.cloudflarestorage.com
```

### Worker Integration
```javascript
// worker.js - R2 binding example
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const key = url.pathname.slice(1);
    
    try {
      const object = await env.MY_BUCKET.get(key);
      
      if (!object) {
        return new Response('Not found', { status: 404 });
      }
      
      const headers = new Headers();
      object.writeHttpMetadata(headers);
      
      return new Response(object.body, { headers });
    } catch (error) {
      return new Response('Error: ' + error.message, { status: 500 });
    }
  }
};
```

### Wrangler.toml Configuration
```toml
name = "r2-worker"
main = "worker.js"

[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-bucket"
```

## Decision Tree

**What's your R2 use case?**
- **Static file serving** → Set up custom domain + public bucket policy
- **Private storage** → Use presigned URLs or Worker authentication
- **Backup/archive** → Use S3 sync commands with lifecycle rules
- **Image hosting** → Custom domain + Image Resizing Worker
- **API file uploads** → Workers with R2 bindings + authentication

**Access pattern?**
- **Public read** → Bucket policy with public GetObject
- **Authenticated access** → Workers with auth logic
- **Time-limited access** → Presigned URLs
- **Admin access** → Direct API tokens

## Common Patterns

### Image Upload Worker
```javascript
export default {
  async fetch(request, env) {
    if (request.method === 'POST') {
      const formData = await request.formData();
      const file = formData.get('image');
      
      if (!file) {
        return new Response('No file uploaded', { status: 400 });
      }
      
      const key = `images/${Date.now()}-${file.name}`;
      
      await env.MY_BUCKET.put(key, file.stream(), {
        httpMetadata: {
          contentType: file.type,
          cacheControl: 'max-age=31536000'
        }
      });
      
      return new Response(JSON.stringify({ 
        url: `https://files.example.com/${key}` 
      }), {
        headers: { 'Content-Type': 'application/json' }
      });
    }
    
    return new Response('Method not allowed', { status: 405 });
  }
};
```

### Bulk Upload Script
```bash
#!/bin/bash
# bulk-upload.sh

BUCKET_NAME="my-bucket"
SOURCE_DIR="./uploads"

find "$SOURCE_DIR" -type f | while read file; do
  # Get relative path
  rel_path=${file#$SOURCE_DIR/}
  
  echo "Uploading $file to $rel_path"
  wrangler r2 object put "$BUCKET_NAME/$rel_path" --file "$file"
done
```

## Troubleshooting

### Common Errors

**Error: "Bucket not found"**
```bash
# Check if bucket exists
wrangler r2 bucket list

# Verify account ID in wrangler.toml
wrangler whoami
```

**Error: "Access denied"**
```bash
# Check API token permissions
# Token needs: Zone:Zone Settings:Edit, Zone:Zone:Read, Account:Cloudflare Workers:Edit, Account:Account Settings:Read

# Re-authenticate
wrangler logout
wrangler login
```

**Error: "Object too large"**
```bash
# R2 has 5GB single object limit
# Use multipart upload for larger files via S3 API:
aws s3 cp large-file.zip s3://my-bucket/ --endpoint-url=$AWS_ENDPOINT_URL
```

**Error: "CORS policy"**
```javascript
// Add CORS headers in Worker
const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
  'Access-Control-Allow-Headers': 'Content-Type',
};

return new Response(object.body, { 
  headers: { ...headers, ...corsHeaders } 
});
```

### Performance Issues

**Slow uploads/downloads:**
```bash
# Use multipart uploads for files >100MB
aws configure set default.s3.max_concurrent_requests 20
aws configure set default.s3.multipart_threshold 64MB
aws configure set default.s3.multipart_chunksize 16MB
```

**High request costs:**
```bash
# Implement caching in Workers
# Use Cache API or KV for frequently accessed metadata
# Batch operations when possible
```

### Debug Commands
```bash
# Verbose wrangler output
wrangler r2 object put my-bucket/test.txt --file ./test.txt --verbose

# Check object metadata
wrangler r2 object head my-bucket/test.txt

# Monitor usage
# Check R2 dashboard for storage metrics and request counts
```
