mcpField
C
cloudflare-r2

cloudflare-r2

github.com/jackspaceクラウド・DevOps1651 回インストール12 週間前に更新
リポジトリ

cloudflare-r2 について

cloudflare-r2 は、Workers から Cloudflare R2 Object Storage を利用するためのスキル兼実装ガイドです。オブジェクト操作に加え、マルチパート転送、署名付き URL、メタデータ、ブラウザー向け CORS 設定を扱います。

主な機能

  1. 01Worker のバケットバインディング設定
  2. 02オブジェクトのアップロード、取得、一覧、削除
  3. 03マルチパートおよび再開可能なアップロード
  4. 04署名付きアップロード・ダウンロード URL
  5. 05HTTP メタデータと CORS の設定

活用シーン

  1. 01Worker のオブジェクト保存・取得エンドポイントを構築
  2. 02大容量または再開可能なファイルアップロードに対応
  3. 03署名付き URL でクライアントから直接ファイル転送

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jackspace/claudeskillz --skill cloudflare-r2

インストール後、Claude Code で/cloudflare-r2を入力して読み込まれたか確認します。

同梱ファイルREADME.mdSKILL.json

詳細

Cloudflare R2 オブジェクトストレージ

ステータス: 本番環境で利用可能 ✅ 最終更新日: 2025-10-21 依存関係: cloudflare-worker-base(Worker のセットアップ用) 最新バージョン: [email protected], @cloudflare/[email protected], [email protected]


クイックスタート(5 分)

1. R2 バケットを作成する
# Via Wrangler CLI (recommended)
npx wrangler r2 bucket create my-bucket

# Or via Cloudflare Dashboard
# https://dash.cloudflare.com → R2 Object Storage → Create bucket

バケットの命名規則:

  • 3~63 文字
  • 小文字、数字、ハイフンのみ
  • 先頭と末尾は英字または数字にする必要があります
  • アカウント内でグローバルに一意である必要があります
2. R2 バインディングを設定する

wrangler.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
    }
  ]
}

重要:

  • binding は、コード内でバケットにアクセスする方法です(env.MY_BUCKET)
  • bucket_name は実際の R2 バケット名です
  • preview_bucket_name は任意ですが、開発環境と本番環境のデータを分ける場合におすすめです
3. 基本的なアップロード/ダウンロード
// 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
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

型定義
// 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() - オブジェクトをアップロードする

シグネチャ:

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

基本的な使い方:

// 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());

メタデータを指定する場合:

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',
  },
});

条件付きアップロード(上書きを防止):

// 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,
  },
});

チェックサムを指定する場合:

// R2 will verify the checksum
const md5Hash = await crypto.subtle.digest('MD5', fileData);

await env.MY_BUCKET.put('file.txt', fileData, {
  md5: md5Hash,
});
get() - オブジェクトをダウンロードする

シグネチャ:

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

基本的な使い方:

// 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,
  },
});

異なる形式で読み取る:

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
}

範囲リクエスト(一部ダウンロード):

// 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 },
});

条件付きダウンロード:

// 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() - メタデータのみを取得する

シグネチャ:

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

使い方:

// 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,
  });
}

ユースケース:

  • ファイルが存在するか確認する
  • ダウンロード前にファイルサイズを取得する
  • 最終更新日を確認する
  • キャッシュ用に etag を検証する
delete() - オブジェクトを削除する

シグネチャ:

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

単一削除:

// Delete single object
await env.MY_BUCKET.delete('file.txt');

// No error if file doesn't exist (idempotent)

一括削除(最大 1000 個のキー):

// 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

確認付きで削除する:

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(options?: R2ListOptions): Promise<R2Objects>

基本的な一覧表示:

// 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`);
}

ページネーション:

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,
  });
});

プレフィックスによるフィルタリング(ディレクトリ別に一覧表示):

// 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}/`,
});

区切り文字(フォルダー形式の一覧表示):

// 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/

マルチパートアップロード

100MB を超えるファイルや、中断後に再開できるアップロードには、マルチパートアップロード API を使用します。

マルチパートアップロードを使う場合

✅ 次の場合はマルチパートを使用します:

  • ファイルサイズが 100MB を超える
  • 再開可能なアップロードが必要
  • アップロードを並列化したい
  • ブラウザー/クライアントからアップロードする

❌ 次の場合はマルチパートを使用しないでください:

  • ファイルサイズが 5MB 未満(オーバーヘッドに見合いません)
  • Worker 内でアップロードする(直接 put() を使用してください)
基本的なマルチパートアップロードの流れ
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 });
});
マルチパートアップロードの完全な例
// 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');

    switch (request.method) {
      case 'POST': {
        switch (action) {
          case 'mpu-create': {
            // Create multipart upload
            const multipart = await env.MY_BUCKET.createMultipartUpload(key);
            return Response.json({
              key: multipart.key,
              uploadId: multipart.uploadId,
            });
          }
          case 'mpu-complete': {
            // Complete multipart upload
            const uploadId = url.searchParams.get('uploadId')!;
            const multipart = env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
            const parts = await request.json();
            const object = await multipart.complete(parts);
            return Response.json({
              key: object.key,
              etag: object.etag,
              size: object.size,
            });
          }
          default:
            return new Response(`Unknown action: ${action}`, { status: 400 });
        }
      }
      case 'PUT': {
        switch (action) {
          case 'mpu-uploadpart': {
            // Upload a part
            const uploadId = url.searchParams.get('uploadId')!;
            const partNumber = parseInt(url.searchParams.get('partNumber')!);
            const multipart = env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
            const uploadedPart = await multipart.uploadPart(partNumber, request.body!);
            return Response.json({
              partNumber: uploadedPart.partNumber,
              etag: uploadedPart.etag,
            });
          }
          default:
            return new Response(`Unknown action: ${action}`, { status: 400 });
        }
      }
      case 'DELETE': {
        switch (action) {
          case 'mpu-abort': {
            // Abort multipart upload
            const uploadId = url.searchParams.get('uploadId')!;
            const multipart = env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
            await multipart.abort();
            return new Response(null, { status: 204 });
          }
          default:
            return new Response(`Unknown action: ${action}`, { status: 400 });
        }
      }
      default:
        return new Response('Method Not Allowed', { status: 405 });
    }
  },
} satisfies ExportedHandler<Env>;

署名付き URL

署名付き URL を使うと、Worker を経由せずに、クライアントから R2 へオブジェクトを直接アップロード/ダウンロードできます。

署名付き URL を使う場合

✅ 次の場合は署名付き URL を使用します:

  • クライアントから R2 に直接アップロードする(Worker の帯域幅を節約できます)
  • 非公開オブジェクトへの一時的なアクセスを許可する
  • 有効期限付きのダウンロードリンクを共有する
  • Worker のリクエストサイズ制限を回避する

❌ 次の場合は署名付き URL を使用しないでください:

  • アップロードを処理/検証する必要がある
  • ファイルがすでに公開されている
  • カスタムドメインを使用する(署名付き URL は R2 エンドポイントでのみ機能します)
aws4fetch で署名付き URL を生成する
npm install aws4fetch
import { AwsClient } from 'aws4fetch';

interface Env {
  R2_ACCESS_KEY_ID: string;
  R2_SECRET_ACCESS_KEY: string;
  ACCOUNT_ID: string;
  MY_BUCKET: R2Bucket;
}

const app = new Hono<{ Bindings: Env }>();

app.post('/api/presigned-upload', async (c) => {
  const { filename } = await c.req.json();

  // Create AWS client for R2
  const r2Client = new AwsClient({
    accessKeyId: c.env.R2_ACCESS_KEY_ID,
    secretAccessKey: c.env.R2_SECRET_ACCESS_KEY,
  });

  const bucketName = 'my-bucket';
  const accountId = c.env.ACCOUNT_ID;

  const url = new URL(
    `https://${bucketName}.${accountId}.r2.cloudflarestorage.com/${filename}`
  );

  // Set expiry (1 hour)
  url.searchParams.set('X-Amz-Expires', '3600');

  // Sign the URL for PUT
  const signed = await r2Client.sign(
    new Request(url, { method: 'PUT' }),
    { aws: { signQuery: true } }
  );

  return c.json({
    uploadUrl: signed.url,
    expiresIn: 3600,
  });
});

app.post('/api/presigned-download', async (c) => {
  const { filename } = await c.req.json();

  const r2Client = new AwsClient({
    accessKeyId: c.env.R2_ACCESS_KEY_ID,
    secretAccessKey: c.env.R2_SECRET_ACCESS_KEY,
  });

  const bucketName = 'my-bucket';
  const accountId = c.env.ACCOUNT_ID;

  const url = new URL(
    `https://${bucketName}.${accountId}.r2.cloudflarestorage.com/${filename}`
  );

  url.searchParams.set('X-Amz-Expires', '3600');

  const signed = await r2Client.sign(
    new Request(url, { method: 'GET' }),
    { aws: { signQuery: true } }
  );

  return c.json({
    downloadUrl: signed.url,
    expiresIn: 3600,
  });
});
署名付き URL を使ったクライアント側アップロード
// 1. Get presigned URL from your Worker
const response = await fetch('https://my-worker.workers.dev/api/presigned-upload', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ filename: 'photo.jpg' }),
});

const { uploadUrl } = await response.json();

// 2. Upload file directly to R2
const file = document.querySelector('input[type="file"]').files[0];

await fetch(uploadUrl, {
  method: 'PUT',
  body: file,
  headers: {
    'Content-Type': file.type,
  },
});
署名付き URL のセキュリティ

重要:

  • ❌ クライアント側のコードで R2 アクセスキーを絶対に公開しないでください
  • ✅ 署名付き URL は必ずサーバー側で生成してください
  • ✅ 適切な有効期限を必ず設定してください(通常は 1~24 時間)
  • ✅ URL を生成する前に認証を追加することを検討してください
  • ✅ 署名付き URL の生成にレート制限を設けることを検討してください
// Example with auth check
app.post('/api/presigned-upload', async (c) => {
  // Verify user is authenticated
  const authHeader = c.req.header('Authorization');
  if (!authHeader) {
    return c.json({ error: 'Unauthorized' }, 401);
  }

  // Validate user has permission
  const userId = await verifyToken(authHeader);
  if (!userId) {
    return c.json({ error: 'Invalid token' }, 401);
  }

  // Only allow uploads to user's own folder
  const { filename } = await c.req.json();
  const key = `users/${userId}/${filename}`;

  // Generate presigned URL...
});

CORS 設定

ブラウザーから R2 バケットへのリクエストを許可するには、CORS を設定します。

公開バケットの CORS
{
  "CORSRules": [
    {
      "AllowedOrigins": ["https://example.com"],
      "AllowedMethods": ["GET", "HEAD"],
      "AllowedHeaders": ["*"],
      "MaxAgeSeconds": 3600
    }
  ]
}
すべてのオリジンを許可する(公開アセット)
{
  "CORSRules": [
    {
      "AllowedOrigins": ["*"],
      "AllowedMethods": ["GET", "HEAD"],
      "AllowedHeaders": ["Range"],
      "MaxAgeSeconds": 3600
    }
  ]
}
CORS を使ったアップロード
{
  "CORSRules": [
    {
      "AllowedOrigins": ["https://app.example.com"],
      "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
      "AllowedHeaders": [
        "Content-Type",
        "Content-MD5",
        "x-amz-meta-*"
      ],
      "ExposeHeaders": ["ETag"],
      "MaxAgeSeconds": 3600
    }
  ]
}
ダッシュボードから CORS を適用する
  1. Cloudflare Dashboard → R2 に移動します
  2. バケットを選択します
  3. Settings タブに移動します
  4. CORS Policy で Add CORS policy を選択します
  5. JSON 設定を貼り付けます
  6. 保存します
署名付き URL の CORS

署名付き URL を使う場合、CORS は R2 が直接処理します。Worker ではなく、バケットで CORS を設定してください。


HTTP メタデータ

Content-Type
// Set content type on upload
await env.MY_BUCKET.put('image.jpg', imageData, {
  httpMetadata: {
    contentType: 'image/jpeg',
  },
});

// Will be returned in Content-Type header when downloaded
Cache-Control
// Set caching headers
await env.MY_BUCKET.put('static/logo.png', logoData, {
  httpMetadata: {
    contentType: 'image/png',
    cacheControl: 'public, max-age=31536000, immutable',
  },
});

// For frequently updated content
await env.MY_BUCKET.put('api/data.json', jsonData, {
  httpMetadata: {
    contentType: 'application/json',
    cacheControl: 'public, max-age=60, must-revalidate',
  },
});
Content-Disposition
// Force download with specific filename
await env.MY_BUCKET.put('report.pdf', pdfData, {
  httpMetadata: {
    contentType: 'application/pdf',
    contentDisposition: 'attachment; filename="monthly-report.pdf"',
  },
});

// Display inline
await env.MY_BUCKET.put('image.jpg', imageData, {
  httpMetadata: {
    contentType: 'image/jpeg',
    contentDisposition: 'inline',
  },
});
Content-Encoding
// Indicate gzip compression
await env.MY_BUCKET.put('data.json.gz', gzippedData, {
  httpMetadata: {
    contentType: 'application/json',
    contentEncoding: 'gzip',
  },
});

カスタムメタデータ

オブジェクトに任意のキーと値のメタデータを保存します。

カスタムメタデータの設定
await env.MY_BUCKET.put('document.pdf', pdfData, {
  customMetadata: {
    userId: '12345',
    department: 'engineering',
    uploadDate: new Date().toISOString(),
    version: '1.0',
    approved: 'true',
  },
});
カスタムメタデータの読み取り
const object = await env.MY_BUCKET.head('document.pdf');

if (object) {
  console.log(object.customMetadata);
  // {
  //   userId: '12345',
  //   department: 'engineering',
  //   uploadDate: '2025-10-21T10:00:00.000Z',
  //   version: '1.0',
  //   approved: 'true'
  // }
}
制限事項
  • すべてのカスタムメタデータの合計サイズは最大 2KB です
  • キーと値は文字列である必要があります
  • キーでは大文字と小文字が区別されません
  • キーの数に制限はありません(合計 2KB 以内)

エラー処理

R2 でよくあるエラー
try {
  await env.MY_BUCKET.put(key, data);
} catch (error: any) {
  const message = error.message;

  if (message.includes('R2_ERROR')) {
    // Generic R2 error
  } else if (message.includes('exceeded')) {
    // Quota exceeded
  } else if (message.includes('precondition')) {
    // Conditional operation failed
  } else if (message.includes('multipart')) {
    // Multipart upload error
  }

  console.error('R2 Error:', message);
  return c.json({ error: 'Storage operation failed' }, 500);
}
リトライロジック
async function r2WithRetry<T>(
  operation: () => Promise<T>,
  maxRetries = 3
): Promise<T> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error: any) {
      const message = error.message;

      // Retry on transient errors
      const isRetryable =
        message.includes('network') ||
        message.includes('timeout') ||
        message.includes('temporarily unavailable');

      if (!isRetryable || attempt === maxRetries - 1) {
        throw error;
      }

      // Exponential backoff
      const delay = Math.min(1000 * Math.pow(2, attempt), 5000);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw new Error('Retry logic failed');
}

// Usage
const object = await r2WithRetry(() =>
  env.MY_BUCKET.get('important-file.txt')
);

パフォーマンスの最適化

バッチ操作
// ❌ DON'T: Delete files one by one
for (const file of filesToDelete) {
  await env.MY_BUCKET.delete(file);
}

// ✅ DO: Batch delete (up to 1000 keys)
await env.MY_BUCKET.delete(filesToDelete);
大容量ファイルの範囲リクエスト
// Download only the first 10MB of a large video
const object = await env.MY_BUCKET.get('video.mp4', {
  range: { offset: 0, length: 10 * 1024 * 1024 },
});

// Return range to client
return new Response(object.body, {
  status: 206, // Partial Content
  headers: {
    'Content-Type': 'video/mp4',
    'Content-Range': `bytes 0-${10 * 1024 * 1024 - 1}/${object.size}`,
  },
});
キャッシュヘッダー
// Immutable assets (hashed filenames)
await env.MY_BUCKET.put('static/app.abc123.js', jsData, {
  httpMetadata: {
    contentType: 'application/javascript',
    cacheControl: 'public, max-age=31536000, immutable',
  },
});

// Dynamic content
await env.MY_BUCKET.put('api/latest.json', jsonData, {
  httpMetadata: {
    contentType: 'application/json',
    cacheControl: 'public, max-age=60, stale-while-revalidate=300',
  },
});
データ整合性のためのチェックサム
// Compute MD5 checksum
const md5Hash = await crypto.subtle.digest('MD5', fileData);

// R2 will verify checksum on upload
await env.MY_BUCKET.put('important.dat', fileData, {
  md5: md5Hash,
});

// If checksum doesn't match, upload will fail

ベストプラクティスのまとめ

✅ 必ず行うこと:
  1. すべてのアップロードで適切な contentType を設定する
  2. 複数のオブジェクトにはバッチ削除を使用する(最大 1000 件)
  3. 静的アセットにはキャッシュヘッダー(cacheControl)を設定する
  4. クライアントからの大容量アップロードには署名付き URL を使用する
  5. 100MB を超えるファイルにはマルチパートアップロードを使用する
  6. ブラウザーからのアップロードを許可する前に CORS ポリシーを設定する
  7. 署名付き URL に有効期限を設定する(1~24 時間)
  8. try/catch でエラーを適切に処理する
  9. メタデータのみが必要な場合は、get() ではなく head() を使用する
  10. 上書きを防ぐため、条件付き操作を使用する
❌ 絶対に行わないこと:
  1. R2 のアクセスキーをクライアント側コードで公開しない
  2. contentType の設定を省略しない(ファイルがバイナリとしてダウンロードされます)
  3. ループ内で削除しない(バッチ削除を使用してください)
  4. エラー処理なしでアップロードしない
  5. ブラウザーからのアップロードで CORS の設定を省略しない
  6. 小さなファイル(< 5MB)にマルチパートを使用しない
  7. 1 回の呼び出しで 1000 件を超えるキーを削除しない(失敗します)
  8. アップロードの成功を前提にしない(必ずレスポンスを確認してください)
  9. 署名付き URL の有効期限設定を省略しない(セキュリティ上のリスクになります)
  10. バケット名をハードコードしない(バインディングを使用してください)

防止できる既知の問題

問題説明回避方法
ブラウザーでの CORS エラーCORS ポリシーがないため、ブラウザーからアップロードまたはダウンロードできませんブラウザーからアクセスする前に、バケットの設定で CORS を構成します
ファイルがバイナリとしてダウンロードされるcontent-type が設定されていないと、ブラウザーでファイルが表示されず、ダウンロードされますアップロード時に必ず httpMetadata.contentType を設定します
署名付き URL の有効期限URL の有効期限が切れず、セキュリティ上のリスクになります必ず X-Amz-Expires を設定します(通常 1~24 時間)
マルチパートアップロードの制限パートが 100MB を超えるか、パート数が 10,000 を超えていますパートのサイズを 5MB~100MB にし、アップロードあたりの最大パート数を 10,000 にします
一括削除の制限1000 件を超えるキーを削除しようとすると失敗します削除対象を 1000 件ずつのバッチに分割します
カスタムメタデータの超過メタデータが 2KB の上限を超えていますカスタムメタデータの合計を 2KB 未満に保ちます

Wrangler コマンドリファレンス

# Bucket management
wrangler r2 bucket create <BUCKET_NAME>
wrangler r2 bucket list
wrangler r2 bucket delete <BUCKET_NAME>

# Object management
wrangler r2 object put <BUCKET_NAME>/<KEY> --file=<FILE_PATH>
wrangler r2 object get <BUCKET_NAME>/<KEY> --file=<OUTPUT_PATH>
wrangler r2 object delete <BUCKET_NAME>/<KEY>

# List objects
wrangler r2 object list <BUCKET_NAME>
wrangler r2 object list <BUCKET_NAME> --prefix="folder/"

公式ドキュメント


R2 への保存を始めましょう! 🚀

コメント