Claude-skill-registry cloudflare-r2

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/cloudflare-r2" ~/.claude/skills/majiayu000-claude-skill-registry-cloudflare-r2-f3478a && rm -rf "$T"
manifest: skills/data/cloudflare-r2/SKILL.md
source content

Critical Patterns

Setup Binding

# wrangler.toml
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-bucket"
export interface Env {
  MY_BUCKET: R2Bucket
}

Upload

// Upload file
await env.MY_BUCKET.put("path/to/file.txt", fileBuffer, {
  httpMetadata: {
    contentType: "text/plain",
    cacheControl: "max-age=31536000"
  },
  customMetadata: {
    uploadedBy: "user-123",
    originalName: "document.txt"
  }
})

// From FormData
app.post("/upload", async (c) => {
  const formData = await c.req.formData()
  const file = formData.get("file") as File
  
  if (!file) {
    return c.json({ error: "No file" }, 400)
  }
  
  const key = `uploads/${crypto.randomUUID()}-${file.name}`
  await c.env.MY_BUCKET.put(key, await file.arrayBuffer(), {
    httpMetadata: { contentType: file.type }
  })
  
  return c.json({ success: true, key })
})

Download

// Get object
const object = await env.MY_BUCKET.get("path/to/file.txt")

if (!object) {
  return new Response("Not found", { status: 404 })
}

// Stream to response (efficient for large files)
return new Response(object.body, {
  headers: {
    "Content-Type": object.httpMetadata.contentType,
    "ETag": object.etag,
    "Cache-Control": "max-age=3600"
  }
})

// Access metadata
console.log(object.key)              // File key
console.log(object.size)             // Size in bytes
console.log(object.uploaded)         // Upload date
console.log(object.customMetadata)   // Custom metadata

List Objects

// List with pagination
const listed = await env.MY_BUCKET.list({
  limit: 100,                    // Max 1000
  prefix: "images/",             // Filter by prefix
  cursor: cursorToken,           // For pagination
  include: ["customMetadata"]    // Include metadata
})

for (const obj of listed.objects) {
  console.log(obj.key, obj.size, obj.uploaded)
}

// Next page
if (listed.truncated) {
  const nextPage = await env.MY_BUCKET.list({
    cursor: listed.cursor
  })
}

Delete

// Delete single
await env.MY_BUCKET.delete("path/to/file.txt")

// Delete multiple
await env.MY_BUCKET.delete([
  "file1.txt",
  "file2.txt",
  "images/photo.jpg"
])

Check Existence (Head)

// Get metadata without downloading body
const object = await env.MY_BUCKET.head("file.txt")

if (!object) {
  return new Response("Not found", { status: 404 })
}

console.log(object.size)
console.log(object.etag)
// Note: object.body is undefined for head()

Presigned URLs

import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"

const S3 = new S3Client({
  region: "auto",
  endpoint: `https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: env.R2_ACCESS_KEY_ID,
    secretAccessKey: env.R2_SECRET_ACCESS_KEY
  }
})

// Download URL (valid 1 hour)
const downloadUrl = await getSignedUrl(
  S3,
  new GetObjectCommand({
    Bucket: "my-bucket",
    Key: "file.txt"
  }),
  { expiresIn: 3600 }
)

// Upload URL
const uploadUrl = await getSignedUrl(
  S3,
  new PutObjectCommand({
    Bucket: "my-bucket",
    Key: "upload.txt",
    ContentType: "text/plain"
  }),
  { expiresIn: 3600 }
)

// Client can PUT/GET to these URLs

Performance Tips

// ✅ Stream large files (don't load into memory)
const object = await env.MY_BUCKET.get("large-file.mp4")
return new Response(object.body, {
  headers: { "Content-Type": "video/mp4" }
})

// ✅ Use head() for existence checks (no body transfer)
const exists = await env.MY_BUCKET.head("file.txt") !== null

// ❌ Wasteful - downloads entire file
const exists = await env.MY_BUCKET.get("file.txt") !== null

// ✅ Set cache headers for static files
await env.MY_BUCKET.put("logo.png", file, {
  httpMetadata: {
    contentType: "image/png",
    cacheControl: "public, max-age=31536000, immutable"
  }
})

Common Patterns

File Download Endpoint

app.get("/files/:key", async (c) => {
  const key = c.req.param("key")
  const object = await c.env.MY_BUCKET.get(key)
  
  if (!object) {
    return c.json({ error: "Not found" }, 404)
  }
  
  return new Response(object.body, {
    headers: {
      "Content-Type": object.httpMetadata.contentType,
      "Content-Disposition": `attachment; filename="${object.customMetadata?.originalName}"`,
      "ETag": object.etag
    }
  })
})

List Files with Pagination

app.get("/files", async (c) => {
  const cursor = c.req.query("cursor")
  const prefix = c.req.query("prefix")
  
  const listed = await c.env.MY_BUCKET.list({
    limit: 100,
    prefix,
    cursor,
    include: ["customMetadata"]
  })
  
  return c.json({
    files: listed.objects.map(obj => ({
      key: obj.key,
      size: obj.size,
      uploaded: obj.uploaded,
      metadata: obj.customMetadata
    })),
    cursor: listed.truncated ? listed.cursor : null
  })
})

Commands

# Create bucket
wrangler r2 bucket create my-bucket

# Upload file
wrangler r2 object put my-bucket/file.txt --file=./local.txt

# List objects
wrangler r2 object list my-bucket

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

Resources