
Cloudflare R2 模式实战指南流式传输、条件 GET、分片上传与客户端直传最佳实践【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skillsR2 是 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): PromiseResponse { 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-s3configuration.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必须显式设置为autoR2 用它作为占位值缺失会直接导致所有 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。customMetadataR2 以Recordstring, string存储用户自定义元数据这里记录了上传时间戳与cf-connecting-ipCloudflare 注入的真实客户端 IP为后续审计与溯源留痕。注意限制单对象自定义元数据上限为 2 KB见下文限制表。从 api.md 的R2PutOptions接口看PUT 还支持storageClassStandard | InfrequentAccess与ssecKeySSE-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.mdinterface R2MultipartUpload { key: string; uploadId: string; uploadPart(partNumber: number, value: ReadableStream | ArrayBuffer | string | Blob): PromiseR2UploadedPart; abort(): Promisevoid; complete(uploadedParts: R2UploadedPart[]): PromiseR2Object; }分片大小取 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 SDKR2 提供 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 TokenWorker 运行时使用「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): PromiseResponse { 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-age31536000, 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-age31536000, immutable一年不可变缓存与模式一的 ETag 头配合可实现高效的浏览器/边缘双层缓存cache.put(cacheKey, response.clone())之所以要clone()是因为 Response body 是单次消费流——原始响应要继续返回给客户端缓存写入必须基于克隆体用ctx.waitUntil把写入放到请求生命周期之外异步完成不阻塞响应返回。模式九公开 Bucket 与自定义域名自定义域名 CORS 的公开访问 Worker需要把 Bucket 作为公开静态资源站对外服务时可以自建一个带 CORS 预检、索引重定向与长期缓存的 Workerexport default { async fetch(request: Request, env: Env): PromiseResponse { // 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-age31536000, 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 中的 CLIwrangler r2 bucket domain add my-bucket --domainfiles.example.com。限制原文明确标注r2.dev 公共 URL无鉴权、CORS 只能做 Bucket 级配置、无法覆盖缓存策略。因此对需要细粒度 CORS、缓存控制或访问控制的生产场景应优先选择模式九的 Worker 方案r2.dev 适合内部联调或非敏感数据。实战组合把模式串成一条上传-处理-分发链路将上述模式组合即可得到一条完整的生产链路客户端上传Worker 签发预签名 URL模式七客户端直传 R2Worker 不经手流量异步处理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 \| nullget(key, options?)下载对象支持 range / onlyIf / ssecKeyR2ObjectBody \| R2Object \| nullhead(key)仅取元数据R2Object \| nulldelete(keys)删除对象支持批量Promisevoidlist(options?)列举对象limit/prefix/cursor/delimiter/includeR2ObjectscreateMultipartUpload(key)创建分片上传R2MultipartUploadCLI 侧对应api.mdwrangler 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 --prefixphotos/硬性限制来自 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 credentialsS3Client缺少region: autoList compatibility errorcompatibility_date早于 2022-08-04或未启用r2_list_honor_include标志Multipart 上传失败分片尺寸不统一或 partNumber 不从 1 开始。参考资料R2 patterns.md本文核心文档R2 概览与快速开始R2 API 参考接口与 CLIR2 配置指南绑定、CORS、生命周期、TokenR2 陷阱与排障cloudflare-deploy Skill 入口关联存储参考queuesR2 事件通知消费、workersWorker 运行时【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。