SvelteKit × Cloudflare R2 でファイルアップロードを実装した話
SvelteKit + Cloudflare Workers の構成でCloudflare R2を使ったファイルアップロード機能を実装しました。
Cloudflare R2 とは
R2はCloudflareのオブジェクトストレージサービスです。AWS S3互換のAPIを持ちながら、エグレス料金(転送料金)が無料なのが特徴です。
無料枠
| 項目 | 無料枠 |
|---|---|
| ストレージ | 10 GB / 月 |
| 書き込み操作 | 1,000,000 回 / 月 |
| 読み取り操作 | 10,000,000 回 / 月 |
セットアップ
1. Cloudflareダッシュボードでのr2有効化
初回利用時はダッシュボードから R2 Object Storage を有効化する必要があります。クレジットカードの登録が必要ですが、無料枠内では課金されません。
2. R2バケットを作成
npx wrangler r2 bucket create sveltekit-blog-uploads
--local をつけずに実行すると本番Cloudflareにバケットが作成されます。
3. wrangler.jsonc にバインディングを追加
"r2_buckets": [
{
"binding": "BUCKET",
"bucket_name": "sveltekit-blog-uploads"
}
]
4. tsconfig.json に @cloudflare/workers-types を追加
R2Object などのCloudflare固有の型を認識させるために追加します。
{
"compilerOptions": {
"types": ["vitest/globals", "@cloudflare/workers-types"]
}
}
5. src/app.d.ts に型を追加
interface Platform {
env: {
DB: D1Database;
BUCKET: R2Bucket; // ← 追加
GITHUB_CLIENT_ID: string;
GITHUB_CLIENT_SECRET: string;
};
}
実装
サーバー側(+page.server.ts)
import type { PageServerLoad, Actions } from './$types';
import { fail } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ platform }) => {
const bucket = platform!.env.BUCKET;
const list = await bucket.list();
return {
files: list.objects.map((obj: R2Object) => ({
key: obj.key,
size: obj.size,
}))
};
};
export const actions: Actions = {
upload: async ({ request, platform }) => {
const data = await request.formData();
const file = data.get('file') as File;
if (!file || file.size === 0) {
return fail(400, { error: 'ファイルを選択してください' });
}
const bucket = platform!.env.BUCKET;
await bucket.put(file.name, file.stream(), {
httpMetadata: { contentType: file.type },
});
},
delete: async ({ request, platform }) => {
const data = await request.formData();
const key = data.get('key') as string;
const bucket = platform!.env.BUCKET;
await bucket.delete(key);
},
};
ポイント:load でR2オブジェクトをそのまま返さない
bucket.list() が返す R2Object はクラスインスタンスなので、SvelteKitがシリアライズできずエラーになります。.map() でプレーンなオブジェクト(POJO)に変換して返す必要があります。
// ❌ これはエラーになる
return { files: list.objects };
// ✅ これが正しい(型も明示する)
return {
files: list.objects.map((obj: R2Object) => ({
key: obj.key,
size: obj.size,
}))
};
クライアント側(+page.svelte)
<script lang="ts">
import { enhance } from '$app/forms';
let { data, form } = $props();
</script>
<h1>ファイルアップロード</h1>
<form method="POST" action="?/upload" enctype="multipart/form-data" use:enhance>
<input type="file" name="file" />
<button type="submit">アップロード</button>
{#if form?.error}
<p style="color: red">{form.error}</p>
{/if}
</form>
<h2>アップロード済みファイル</h2>
<ul>
{#each data.files as file}
<li>
<a href="/upload/{file.key}" target="_blank">{file.key}</a>
({Math.round(file.size / 1024)} KB)
<form method="POST" action="?/delete" use:enhance style="display: inline">
<input type="hidden" name="key" value={file.key} />
<button type="submit">削除</button>
</form>
</li>
{/each}
</ul>
ポイント:enctype="multipart/form-data" が必須
ファイルアップロードのフォームには必ず enctype="multipart/form-data" をつけます。これがないとファイルの中身がサーバーに届きません。
ファイル取得用APIルート(/upload/[key]/+server.ts)
R2は非公開なので、Workerを通じてファイルを返す専用ルートが必要です。
import type { RequestHandler } from './$types';
import { error } from '@sveltejs/kit';
export const GET: RequestHandler = async ({ params, platform }) => {
const bucket = platform!.env.BUCKET;
const object = await bucket.get(params.key);
if (!object) {
error(404, 'ファイルが見つかりません');
}
return new Response(object.body, {
headers: {
'Content-Type': object.httpMetadata?.contentType ?? 'application/octet-stream',
},
});
};
ローカル開発
ローカルではwranglerがR2をシミュレートしてくれます。実体は .wrangler/state/v3/r2/ 以下に保存されます。
npm run build
npx wrangler dev
ローカルのR2の中身はCLIで操作できますが、list サブコマンドは存在しないため、アプリ自体か .wrangler/state/v3/r2/ ディレクトリを直接確認するのが現実的です。
# ファイルをダウンロードして確認
npx wrangler r2 object get sveltekit-blog-uploads/ファイル名 --local --file ./downloaded.png
# ファイルを削除
npx wrangler r2 object delete sveltekit-blog-uploads/ファイル名 --local
本番デプロイ時の注意
初回デプロイ時、R2が有効化されていないと以下のエラーが出ます:
Please enable R2 through the Cloudflare Dashboard. [code: 10042]
ダッシュボードでR2を有効化してから再デプロイすると解決します。バケットの再作成は不要で、デプロイ時に自動的にバインディングが認識されます。
まとめ
| 項目 | 内容 |
|---|---|
| バケット作成 | npx wrangler r2 bucket create <name> |
| バインディング | wrangler.jsonc の r2_buckets に追記 |
| アップロード | bucket.put(key, stream, { httpMetadata }) |
| 一覧取得 | bucket.list() → POJOに変換して返す |
| ファイル取得 | 専用の +server.ts でWorker経由で返す |
| 削除 | bucket.delete(key) |