Cloudflare R2 模式实战指南:流式传输、条件 GET、分片上传与客户端直传最佳实践
【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills
R2 是 Cloudflare 提供的 S3 兼容对象存储服务,以零出口流量费、强一致性读写为特色,是承载大文件存储与分发场景的核心存储选型。本文基于 cloudflare-deploy Skill 的 R2 参考文档 patterns.md,系统拆解流式传输、条件 GET、校验上传、分片上传、批量删除、校验和校验、预签名 URL 客户端直传、Cache API 缓存以及公开 Bucket 等十类高频开发模式,并结合 api.md、configuration.md、gotchas.md 给出源码级参数说明与陷阱预警。读完本文,你将能够在 Workers 中直接实现一套可上线的高性能 R2 读写与分发链路。
阅读前提:R2 绑定与运行环境
patterns.md 中所有代码都基于 Workers 运行时通过env.MY_BUCKET访问 R2 Bucket,因此在讨论模式之前,需要先确认两件事:Binding 配置与 S3 SDK 初始化。
Workers Binding 配置
在 configuration.md 中,绑定通过wrangler.jsonc中的r2_buckets字段声明:
{ "r2_buckets": [ { "binding": "MY_BUCKET", "bucket_name": "my-bucket-name" } ] }随后在 TypeScript 中声明环境类型并直接使用:
interface Env { MY_BUCKET: R2Bucket; } export default { async fetch(request: Request, env: Env): Promise<Response> { const object = await env.MY_BUCKET.get('file.txt'); return new Response(object?.body); } }S3 SDK 初始化(存储类迁移、CORS、预签名 URL 场景)
R2 兼容 S3 REST API,凡是 Workers API 覆盖不到的运维类操作(存储类迁移、Bucket 级 CORS、生命周期规则),都要借助@aws-sdk/client-s3,configuration.md 给出的标准初始化方式如下:
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({ region: 'auto', endpoint: `https://${accountId}.r2.cloudflarestorage.com`, credentials: { accessKeyId: env.R2_ACCESS_KEY_ID, secretAccessKey: env.R2_SECRET_ACCESS_KEY } }); await s3.send(new PutObjectCommand({ Bucket: 'my-bucket', Key: 'file.txt', Body: data, StorageClass: 'STANDARD' // or 'STANDARD_IA' }));这里有两个从 gotchas.md 反查出的关键点:region必须显式设置为'auto',R2 用它作为占位值,缺失会直接导致所有 S3 SDK 调用失败;endpoint 使用账号级域名https://${accountId}.r2.cloudflarestorage.com。
另外,README.md 给出的推荐阅读顺序是README → configuration.md → api.md → patterns.md,即先完成绑定与环境配置,再进入本文的模式实现。
模式一:流式传输大文件
将 R2 对象直接以流(ReadableStream)形式返回给客户端,是大文件分发的基础形态——避免把整个对象读入内存再输出:
const object = await env.MY_BUCKET.get(key); if (!object) return new Response('Not found', { status: 404 }); const headers = new Headers(); object.writeHttpMetadata(headers); headers.set('etag', object.httpEtag); return new Response(object.body, { headers });这段代码的精髓在于两个方法:
object.writeHttpMetadata(headers):把对象存储时写入的 HTTP 元数据(contentType、cacheControl 等,见 api.md 中R2HTTPMetadata接口)批量写入响应头;object.httpEtag:返回带引号的 ETag。这里必须使用httpEtag而非etag——gotchas.md 明确指出,etag是不带引号的裸值,直接塞进响应头会导致协议格式错误。
一个容易踩的坑是流长度未知:gotchas.md 记录了「Stream upload failed / 静默截断」问题——当上游响应的流长度未知且未携带 Content-Length 时,R2 写入可能无报错地被截断。解决方案是先缓冲(await response.arrayBuffer())或在 PUT 时显式传入contentLength。
模式二:条件 GET 实现 304 Not Modified
利用 HTTP 缓存协商,让未变更的对象直接以 304 返回,省去重复下载大文件的流量:
const ifNoneMatch = request.headers.get('if-none-match'); const object = await env.MY_BUCKET.get(key, { onlyIf: { etagDoesNotMatch: ifNoneMatch?.replace(/"/g, '') || '' } }); if (!object) return new Response('Not found', { status: 404 }); if (!object.body) return new Response(null, { status: 304, headers: { 'etag': object.httpEtag } }); return new Response(object.body, { headers: { 'etag': object.httpEtag } });关键机制来自R2GetOptions.onlyIf条件字段(api.md 中的R2Conditional接口):
interface R2Conditional { etagMatches?: string; etagDoesNotMatch?: string; uploadedBefore?: Date; uploadedAfter?: Date; }当etagDoesNotMatch前置条件不满足时,R2 返回的对象不带 body 流,因此 gotchas.md 强调:判断条件是!object.body,而不是!object。客户端传入的If-None-Match头通常带引号(如"abc123"),而 R2 条件比较时用的是不带引号的裸 etag,所以这里先用replace(/"/g, '')剥掉引号再传入。
模式三:带校验的上传(Key 校验与元数据)
把用户上传请求体直接转发给 R2 前,必须先做 Key 安全校验,并顺带写入 HTTP 元数据与自定义元数据:
const key = url.pathname.slice(1); if (!key || key.includes('..')) return new Response('Invalid key', { status: 400 }); const object = await env.MY_BUCKET.put(key, request.body, { httpMetadata: { contentType: request.headers.get('content-type') || 'application/octet-stream' }, customMetadata: { uploadedAt: new Date().toISOString(), ip: request.headers.get('cf-connecting-ip') || 'unknown' } }); return Response.json({ key: object.key, size: object.size, etag: object.httpEtag });- Key 校验:gotchas.md 将
url.pathname.slice(1)直接作为 Key 称为危险做法——攻击者可构造../../../etc/passwd之类路径穿越串。安全校验需同时拦截空 Key、包含..的 Key 以及以/开头的 Key。 httpMetadata:透传客户端声明的 content-type,缺失时回退到application/octet-stream。customMetadata:R2 以Record<string, string>存储用户自定义元数据,这里记录了上传时间戳与cf-connecting-ip(Cloudflare 注入的真实客户端 IP),为后续审计与溯源留痕。注意限制:单对象自定义元数据上限为 2 KB(见下文限制表)。
从 api.md 的R2PutOptions接口看,PUT 还支持storageClass(Standard | InfrequentAccess)与ssecKey(SSE-C 加密)等选项,可作为上传链路的能力补充。
模式四:分片上传与进度回调
大文件(如视频)推荐使用 Multipart Upload:按固定分片大小切分、逐片上传、最后合并,天然支持并发与断点续传:
const PART_SIZE = 5 * 1024 * 1024; // 5MB const partCount = Math.ceil(file.size / PART_SIZE); const multipart = await env.MY_BUCKET.createMultipartUpload(key, { httpMetadata: { contentType: file.type } }); const uploadedParts: R2UploadedPart[] = []; try { for (let i = 0; i < partCount; i++) { const start = i * PART_SIZE; const part = await multipart.uploadPart(i + 1, file.slice(start, start + PART_SIZE)); uploadedParts.push(part); onProgress?.(Math.round(((i + 1) / partCount) * 100)); } return await multipart.complete(uploadedParts); } catch (error) { await multipart.abort(); throw error; }对应底层接口(api.md):
interface R2MultipartUpload { key: string; uploadId: string; uploadPart(partNumber: number, value: ReadableStream | ArrayBuffer | string | Blob): Promise<R2UploadedPart>; abort(): Promise<void>; complete(uploadedParts: R2UploadedPart[]): Promise<R2Object>; }- 分片大小取 5 MB,与 R2 的非末片最小分片限制一致(见下文限制表);
partNumber从 1 开始,gotchas.md 提醒:所有分片尺寸必须一致(末片除外)、未完成的 Multipart 上传会在 7 天后自动中止、resumeMultipartUpload(key, uploadId)不会校验 uploadId 是否存在;- 进度回调在每片完成后按已上传片数占比计算百分比,异常路径统一
abort()清理已上传分片。
模式五:前缀批量删除
清理日志、过期素材等场景需要按前缀批量删除对象。R2 的list单次最多返回 1000 个对象(limit上限),delete单次最多接收 1000 个 Key,因此必须结合游标分页循环:
async function deletePrefix(prefix: string, env: Env) { let cursor: string | undefined; let truncated = true; while (truncated) { const listed = await env.MY_BUCKET.list({ prefix, limit: 1000, cursor }); if (listed.objects.length > 0) { await env.MY_BUCKET.delete(listed.objects.map(o => o.key)); } truncated = listed.truncated; cursor = listed.cursor; } }这里的分页判据必须是listed.truncated布尔标志,而不是objects.length < limit这类对象数量比较。gotchas.md 给出了反例:当list使用include: ['httpMetadata', 'customMetadata']拉取元数据时,单页返回的对象数量可能少于 limit(元数据占用了分页预算),此时按数量判断会提前终止循环、漏删对象。
模式六:校验和校验与存储类迁移
上传时写入 SHA-256 校验和
对数据完整性敏感的场景(备份、数据湖入湖),可在 PUT 时附带校验和:
const hash = await crypto.subtle.digest('SHA-256', data); await env.MY_BUCKET.put(key, data, { sha256: hash });注意 gotchas.md 的限制:每次 PUT 只允许指定一种校验和算法,同时传md5与sha256会直接报错。从R2Checksums接口(api.md)看,R2 支持 md5、sha1、sha256、sha384、sha512 五种。
存储类迁移(需 S3 SDK)
R2 提供 Standard 与 InfrequentAccess 两种存储类(README.md),前者面向高频访问、读取低延迟;后者存储成本更低但产生取回费用,且有 30 天最低计费周期。存储类迁移需要借助 S3 SDK 的 CopyObject 完成:
import { S3Client, CopyObjectCommand } from '@aws-sdk/client-s3'; await s3.send(new CopyObjectCommand({ Bucket: 'my-bucket', Key: key, CopySource: `/my-bucket/${key}`, StorageClass: 'STANDARD_IA' }));gotchas.md 额外提醒三条存储类陷阱:IA 删除早于 30 天仍按 30 天计费、IA → Standard 不能通过生命周期规则反向转换(只能用 CopyObject)、IA 读取会产生取回费用。
模式七:客户端直传(预签名 URL)
将大文件上传流量从 Worker 转移到客户端与 R2 之间直连,Worker 只负责签发短时效的上传凭证,避免经手大流量:
import { S3Client } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { PutObjectCommand } from '@aws-sdk/client-s3'; // Worker: Generate presigned upload URL 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 } }); const url = await getSignedUrl(s3, new PutObjectCommand({ Bucket: 'my-bucket', Key: key }), { expiresIn: 3600 }); return Response.json({ uploadUrl: url }); // Client: Upload directly const { uploadUrl } = await fetch('/api/upload-url').then(r => r.json()); await fetch(uploadUrl, { method: 'PUT', body: file });expiresIn: 3600表示 URL 一小时后失效(预签名 URL 最大有效期 7 天,见限制表);- gotchas.md 建议:URL 签发后过期并不会主动通知客户端,因此应在响应中同时返回
expiresAt,让客户端自行处理过期(否则过期后请求会得到 403); - 浏览器端直传会触发跨域请求,因此还需要配合 Bucket 级 CORS 配置(见 configuration.md 的
PutBucketCorsCommand示例,允许 GET/PUT/HEAD 并暴露ETag头); - 安全上,configuration.md 建议按最小权限拆分 R2 API Token:Worker 运行时使用「Object Read & Write」级别,CORS、生命周期等管理操作单独使用「Admin Read & Write」级别 Token,避免权限扩散。
模式八:Cache API 缓存 R2 对象
R2 对象天然适合叠加 Cloudflare 边缘缓存:第一层命中 Cache API,未命中再回源 R2,并异步把响应写入缓存供后续请求复用:
export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { const cache = caches.default; const url = new URL(request.url); const cacheKey = new Request(url.toString(), request); // Check cache first let response = await cache.match(cacheKey); if (response) return response; // Fetch from R2 const key = url.pathname.slice(1); const object = await env.MY_BUCKET.get(key); if (!object) return new Response('Not found', { status: 404 }); const headers = new Headers(); object.writeHttpMetadata(headers); headers.set('etag', object.httpEtag); headers.set('cache-control', 'public, max-age=31536000, immutable'); response = new Response(object.body, { headers }); // Cache for subsequent requests ctx.waitUntil(cache.put(cacheKey, response.clone())); return response; } };实现要点:
- 缓存键基于完整 URL 构造,
caches.default为全局默认缓存命名空间; - 命中缓存直接返回,回源路径对静态对象设置
cache-control: public, max-age=31536000, immutable(一年不可变缓存),与模式一的 ETag 头配合可实现高效的浏览器/边缘双层缓存; cache.put(cacheKey, response.clone())之所以要clone(),是因为 Response body 是单次消费流——原始响应要继续返回给客户端,缓存写入必须基于克隆体;用ctx.waitUntil把写入放到请求生命周期之外异步完成,不阻塞响应返回。
模式九:公开 Bucket 与自定义域名
自定义域名 + CORS 的公开访问 Worker
需要把 Bucket 作为公开静态资源站对外服务时,可以自建一个带 CORS 预检、索引重定向与长期缓存的 Worker:
export default { async fetch(request: Request, env: Env): Promise<Response> { // CORS preflight if (request.method === 'OPTIONS') { return new Response(null, { headers: { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET, HEAD', 'access-control-max-age': '86400' } }); } const key = new URL(request.url).pathname.slice(1); if (!key) return Response.redirect('/index.html', 302); const object = await env.MY_BUCKET.get(key); if (!object) return new Response('Not found', { status: 404 }); const headers = new Headers(); object.writeHttpMetadata(headers); headers.set('etag', object.httpEtag); headers.set('access-control-allow-origin', '*'); headers.set('cache-control', 'public, max-age=31536000, immutable'); return new Response(object.body, { headers }); } };该模式相对模式一追加了三层逻辑:
- OPTIONS 预检:浏览器跨域请求前会先发预检,这里直接返回 204 级空响应并声明允许的来源、方法与预检缓存时长(
access-control-max-age: 86400秒); - 根路径重定向:空 Key 时 302 跳转到
index.html,实现目录索引语义; - 公开 CORS 与缓存头:响应统一追加
access-control-allow-origin: *,配合一年的 immutable 缓存。
r2.dev 公共 URL 与自定义域名的取舍
不写 Worker 时,R2 也提供开箱即用的公开访问方式(patterns.md 原文):
- 在控制台开启 r2.dev 后获得形如
https://pub-${hashId}.r2.dev/${key}的公共 URL; - 或在控制台绑定自定义域名,得到
https://files.example.com/${key}(亦可使用 configuration.md 中的 CLI:wrangler r2 bucket domain add my-bucket --domain=files.example.com)。
限制(原文明确标注):r2.dev 公共 URL无鉴权、CORS 只能做 Bucket 级配置、无法覆盖缓存策略。因此对需要细粒度 CORS、缓存控制或访问控制的生产场景,应优先选择模式九的 Worker 方案;r2.dev 适合内部联调或非敏感数据。
实战组合:把模式串成一条上传-处理-分发链路
将上述模式组合即可得到一条完整的生产链路:
- 客户端上传:Worker 签发预签名 URL(模式七),客户端直传 R2,Worker 不经手流量;
- 异步处理:R2 事件通知(PutObject/DeleteObject/CompleteMultipartUpload)推送到 Cloudflare Queues,由队列消费端做缩略图生成、病毒扫描等后处理(详见 configuration.md 的
event_notifications配置与 README.md 中的消费端示例); - 分发:读取侧通过 Worker 实现流式返回 + 条件 GET + Cache API 缓存(模式一、二、八),公开内容叠加自定义域名(模式九);
- 生命周期治理:通过 S3 SDK 配置生命周期规则,冷数据 30 天后转 InfrequentAccess、90 天后过期删除(configuration.md),并按前缀批量清理残留(模式五)。
附录:R2 关键接口与硬性限制速查
核心操作一览(来自 api.md)
| 方法 | 用途 | 返回 |
|---|---|---|
put(key, value, options?) | 上传对象 | R2Object \| null |
get(key, options?) | 下载对象(支持 range / onlyIf / ssecKey) | R2ObjectBody \| R2Object \| null |
head(key) | 仅取元数据 | R2Object \| null |
delete(keys) | 删除对象(支持批量) | Promise<void> |
list(options?) | 列举对象(limit/prefix/cursor/delimiter/include) | R2Objects |
createMultipartUpload(key) | 创建分片上传 | R2MultipartUpload |
CLI 侧对应(api.md):
wrangler r2 object put my-bucket/file.txt --file=./local.txt wrangler r2 object get my-bucket/file.txt --file=./download.txt wrangler r2 object delete my-bucket/file.txt wrangler r2 object list my-bucket --prefix=photos/硬性限制(来自 gotchas.md)
| 限制项 | 值 |
|---|---|
| 对象大小 | 5 TB |
| 分片上传片数 | 10,000 |
| 分片最小尺寸 | 5 MB(末片除外) |
| 批量删除 | 1,000 个 Key/次 |
| List 单次返回 | 1,000 个对象 |
| Key 大小 | 1,024 字节 |
| 自定义元数据 | 2 KB/对象 |
| 预签名 URL 最大有效期 | 7 天 |
常见错误速查
- "Stream upload failed" / 静默截断:流长度未知且缺 Content-Length,先缓冲或显式传长度;
- S3 SDK "Invalid credentials":
S3Client缺少region: 'auto'; - "List compatibility error":
compatibility_date早于 2022-08-04,或未启用r2_list_honor_include标志; - Multipart 上传失败:分片尺寸不统一或 partNumber 不从 1 开始。
参考资料
- R2 patterns.md(本文核心文档)
- R2 概览与快速开始
- R2 API 参考(接口与 CLI)
- R2 配置指南(绑定、CORS、生命周期、Token)
- R2 陷阱与排障
- cloudflare-deploy Skill 入口
- 关联存储参考:queues(R2 事件通知消费)、workers(Worker 运行时)
【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考