cloudflare-r2 について
cloudflare-r2 は、Workers から Cloudflare R2 Object Storage を利用するためのスキル兼実装ガイドです。オブジェクト操作に加え、マルチパート転送、署名付き URL、メタデータ、ブラウザー向け CORS 設定を扱います。
主な機能
- 01Worker のバケットバインディング設定
- 02オブジェクトのアップロード、取得、一覧、削除
- 03マルチパートおよび再開可能なアップロード
- 04署名付きアップロード・ダウンロード URL
- 05HTTP メタデータと CORS の設定
活用シーン
- 01Worker のオブジェクト保存・取得エンドポイントを構築
- 02大容量または再開可能なファイルアップロードに対応
- 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 を適用する
- Cloudflare Dashboard → R2 に移動します
- バケットを選択します
- Settings タブに移動します
- CORS Policy で Add CORS policy を選択します
- JSON 設定を貼り付けます
- 保存します
署名付き 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
ベストプラクティスのまとめ
✅ 必ず行うこと:
- すべてのアップロードで適切な
contentTypeを設定する - 複数のオブジェクトにはバッチ削除を使用する(最大 1000 件)
- 静的アセットにはキャッシュヘッダー(
cacheControl)を設定する - クライアントからの大容量アップロードには署名付き URL を使用する
- 100MB を超えるファイルにはマルチパートアップロードを使用する
- ブラウザーからのアップロードを許可する前に CORS ポリシーを設定する
- 署名付き URL に有効期限を設定する(1~24 時間)
- try/catch でエラーを適切に処理する
- メタデータのみが必要な場合は、
get()ではなくhead()を使用する - 上書きを防ぐため、条件付き操作を使用する
❌ 絶対に行わないこと:
- R2 のアクセスキーをクライアント側コードで公開しない
contentTypeの設定を省略しない(ファイルがバイナリとしてダウンロードされます)- ループ内で削除しない(バッチ削除を使用してください)
- エラー処理なしでアップロードしない
- ブラウザーからのアップロードで CORS の設定を省略しない
- 小さなファイル(< 5MB)にマルチパートを使用しない
- 1 回の呼び出しで 1000 件を超えるキーを削除しない(失敗します)
- アップロードの成功を前提にしない(必ずレスポンスを確認してください)
- 署名付き URL の有効期限設定を省略しない(セキュリティ上のリスクになります)
- バケット名をハードコードしない(バインディングを使用してください)
防止できる既知の問題
| 問題 | 説明 | 回避方法 |
|---|---|---|
| ブラウザーでの 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 の概要: https://developers.cloudflare.com/r2/
- はじめに: https://developers.cloudflare.com/r2/get-started/
- Workers API: https://developers.cloudflare.com/r2/api/workers/workers-api-reference/
- マルチパートアップロード: https://developers.cloudflare.com/r2/api/workers/workers-multipart-usage/
- 署名付き URL: https://developers.cloudflare.com/r2/api/s3/presigned-urls/
- CORS の設定: https://developers.cloudflare.com/r2/buckets/cors/
- パブリックバケット: https://developers.cloudflare.com/r2/buckets/public-buckets/
R2 への保存を始めましょう! 🚀