【Cloudflare Workers 全端架構師之路 05】儲存篇:R2 檔案處理與零流量費架構
01. 前言:S3 帳單裡的隱形殺手
在雲端架構中,儲存成本通常由兩部分組成:儲存費 (Storage) 與 流量費 (Egress)。 大家往往只關注儲存費,卻忽略了流量費才是真正的殺手。
情境分析:你經營一個圖片分享網站,儲存了 1TB 的照片。
- AWS S3: 當這些照片被使用者下載 10TB 的流量時,AWS 會向你收取約 $900 USD 的流量費 (以 $0.09/GB 計算)。
- Cloudflare R2: 流量費為 $0。
這就是為什麼 R2 被稱為「S3 殺手」。對於高頻寬應用(影音串流、大型檔案下載、模型權重檔),R2 的架構優勢是壓倒性的。
02. 架構比較:R2 vs. AWS S3
| 特性 | Cloudflare R2 | AWS S3 (Standard) |
|---|---|---|
| API 相容性 | S3 Compatible (可直接用 AWS SDK) | 原生標準 |
| 儲存費用 | $0.015 / GB-month | $0.023 / GB-month |
| 出口流量費 (Egress) | $0 (Free) | ~$0.09 / GB (前 100GB 免費) |
| 存取效能 | 自動分散至邊緣節點 | 需搭配 CloudFront 才能達到邊緣效能 |
| 整合性 | 原生整合 Workers (Binding) | 需透過 IAM Role / Access Key |
架構師筆記: R2 的另一個隱藏優勢是不需要 CDN 設定。S3 通常需要掛一個 CloudFront 來加速並節省流量(雖然還是要錢)。R2 天生就跑在 Cloudflare 的邊緣網路上,這讓架構圖少了一整層複雜度。
03. 實作挑戰:為什麼不能由 Worker 轉傳檔案?
初學者常犯的錯誤是:寫一個 API 接收 multipart/form-data,Worker 讀取檔案內容,再 put 到 R2。
這樣做有兩個致命傷:
- 記憶體限制:Worker 的 Request Body 限制通常是 100MB (付費版)。
- 雙重頻寬:檔案先上傳到 Worker,Worker 再上傳到 R2。這浪費了 Worker 的 CPU 時間 (CPU Time),並增加了延遲。
最佳解法:Presigned URLs (預簽名網址) 讓 Worker 扮演「發證機」,使用者拿到「通行證 (Signed URL)」後,直接對 R2 上傳。
04. 環境建置:R2 + AWS SDK
雖然 Cloudflare 提供了原生的 env.BUCKET API,但為了產生 Presigned URL,我們目前仍需使用 AWS SDK for JavaScript v3。
步驟 1: 建立 Bucket
npx wrangler r2 bucket create my-assets
在 wrangler.toml 設定:
[[r2_buckets]]
binding = "MY_BUCKET" # 原生操作用
bucket_name = "my-assets"
[vars]
# SDK 用 (請從 Dashboard 取得並透過 wrangler secret 設定真實 Key)
R2_ACCOUNT_ID = "你的_ACCOUNT_ID"
R2_BUCKET_NAME = "my-assets"
步驟 2: 安裝 SDK
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
05. 深度實作:安全上傳通道
我們將實作一個嚴格限制 檔案類型 (Content-Type) 與 檔案大小 (Content-Length) 的上傳 API。
修改 src/index.ts:
import { Hono } from 'hono'
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { v4 as uuidv4 } from 'uuid' // 建議用 UUID 生成檔名
type Bindings = {
R2_ACCOUNT_ID: string;
R2_ACCESS_KEY_ID: string; // Secret
R2_SECRET_ACCESS_KEY: string; // Secret
R2_BUCKET_NAME: string;
}
const app = new Hono<{ Bindings: Bindings }>()
const getS3Client = (env: Bindings) => {
return new S3Client({
region: 'auto',
endpoint: `https://${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
},
});
}
// POST: 取得上傳連結
app.post('/upload/sign', async (c) => {
const { contentType, contentLength } = await c.req.json();
// 1. 安全性檢查
// 限制只能上傳圖片
if (!['image/jpeg', 'image/png', 'image/webp'].includes(contentType)) {
return c.json({ error: 'Only images are allowed' }, 400);
}
// 限制大小 (例如 10MB)
if (contentLength > 10 * 1024 * 1024) {
return c.json({ error: 'File too large' }, 400);
}
const s3 = getS3Client(c.env);
const fileKey = `uploads/${uuidv4()}`; // 隨機檔名避免覆蓋
// 2. 產生簽名
const command = new PutObjectCommand({
Bucket: c.env.R2_BUCKET_NAME,
Key: fileKey,
ContentType: contentType,
ContentLength: contentLength, // 強制檢查長度
});
// URL 有效期 5 分鐘
const signedUrl = await getSignedUrl(s3, command, { expiresIn: 300 });
return c.json({
uploadUrl: signedUrl,
key: fileKey
});
})
// GET: 取得私密檔案下載連結
app.get('/file/:key', async (c) => {
const key = c.req.param('key');
const s3 = getS3Client(c.env);
const command = new GetObjectCommand({
Bucket: c.env.R2_BUCKET_NAME,
Key: key,
});
// 下載連結 1 小時有效
const downloadUrl = await getSignedUrl(s3, command, { expiresIn: 3600 });
return c.json({ url: downloadUrl });
})
export default app
06. 前端如何配合?
拿到 uploadUrl 後,前端不能用 FormData 上傳,而是要直接把 File 物件塞進 Body 進行 PUT 請求。
// 前端範例代碼
const file = document.getElementById('fileInput').files[0];
// 1. 取得簽名
const res = await fetch('/upload/sign', {
method: 'POST',
body: JSON.stringify({
contentType: file.type,
contentLength: file.size
})
});
const { uploadUrl } = await res.json();
// 2. 直接上傳 R2 (不經過你的 Worker)
await fetch(uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': file.type },
body: file
});
07. 小結與下一步
我們現在擁有了一個強大的檔案處理系統:
- 省錢:流量費 $0。
- 效能:使用者直接連線 R2 邊緣節點上傳。
- 安全:透過 Worker 嚴格驗證檔案類型與權限。
到目前為止,我們已經具備了 Database (D1)、Storage (R2) 和 Cache (KV)。但在這一切之上,還有一個至關重要的層級我們還沒觸碰:安全性 (Security)。
如果有人惡意刷爆你的 API 怎麼辦?如果我們需要身分驗證 (Login) 怎麼辦?
在下一篇 Part 6: 安全篇,我們將探討如何使用 Hono Middleware 實作 JWT 驗證、CORS 策略 以及 Rate Limiting (速率限制),為你的 Edge 架構穿上防彈衣。

關於作者
Ken Huang
熱衷於嘗試各類新技術的軟體開發者,現階段主力為 Android / iOS 雙平台開發,同時持續深耕前端與後端技術,以成為全端工程師與軟體架構師為目標。
最廣為人知的代表作為 BePTT。開發哲學是「以做身體健康為前提」,致力於在工作與生活平衡的基礎上,打造出擁有絕佳使用體驗的軟體服務。
這裡是用於紀錄與分享開發經驗的空間,希望能透過我的實戰筆記,幫助開發者解決疑難雜症並提升效率。