说明:本文由 Codex 根据作者提供的主题、对话素材与 Cloudflare 官方文档辅助生成,属于 AI 生成/整理内容,非作者原创。请读者自行甄别并交叉验证。
**结论:把博客图片上传到 R2 最稳妥的方式,是使用仅限目标 bucket 的 R2 专用 Object Read & Write token,通过 S3 兼容 endpoint 写入 YYYY/filename.ext,再用已启用的自定义 CDN 域名访问。**上传工具应显式写入 Content-Type 和 Cache-Control,默认拒绝同名覆盖,并在上传后同时校验 R2 元数据与 CDN URL。
本文按 2026-08-07 的 Cloudflare 官方文档整理。它适用于“本地写 Hugo Markdown,图片由 R2 和自定义域名提供”的博客;不适用于需要浏览器直接上传私密文件的业务系统。
先确认访问路径#
flowchart LR A["本地图片"] --> B["AWS CLI / S3 兼容 API"] B --> C["R2 bucket\nYYYY/filename.ext"] C --> D["R2 custom domain\nhttps://cdn.example.com"] D --> E["Hugo Markdown"]
R2 的 S3 API endpoint 是 https://<ACCOUNT_ID>.r2.cloudflarestorage.com,SDK/CLI 的 region 应填 auto。R2 支持 PutObject 的 Content-Type、Cache-Control 等系统元数据;因此无需依赖文件扩展名猜测浏览器响应头。S3 API compatibility aws CLI 示例
生产访问使用 R2 bucket 的自定义域名,而不是 r2.dev。r2.dev 是开发用途且受限;自定义域名需要位于同一 Cloudflare 账户的 zone 中,并在 bucket 的 Settings → Custom Domains 显示为 Active。R2 的 S3 ACL 不提供 public-read,因此不要给 AWS CLI 加 --acl public-read。Public buckets and custom domains
1. 创建最小权限的 R2 凭据#
在 Cloudflare Dashboard 的 R2 Object Storage → Manage API Tokens 创建 R2 专用 API token:
- 权限选择 Object Read & Write。
- 仅 scope 到博客使用的目标 bucket。
- 保存页面显示的 Access Key ID 与 Secret Access Key;Secret 之后无法再次查看。
这两个值是给 S3 兼容客户端使用的凭据,不要把它们放进 Markdown、提交记录、shell history、公开截图或通用 Cloudflare API token 配置中。R2 的 Object Read & Write 权限支持对限定 bucket 的对象读、写和列举;无需授予 bucket 管理权限。R2 Authentication
2. 只在本机配置 .env#
将公开模板复制到本地文件,并保持 .env 被 Git 忽略:
cp .env.example .env配置的结构如下;尖括号值是本机私密配置,不能照抄到文章或仓库。
CLOUDFLARE_ACCOUNT_ID=<cloudflare-account-id>
R2_BUCKET=<blog-static-bucket>
R2_ENDPOINT=https://<cloudflare-account-id>.r2.cloudflarestorage.com
R2_REGION=auto
R2_ACCESS_KEY_ID=<r2-access-key-id>
R2_SECRET_ACCESS_KEY=<r2-secret-access-key>
R2_PUBLIC_BASE_URL=https://<your-public-cdn-domain>
R2_CACHE_CONTROL=public, max-age=31536000, immutable其中只有 R2_ACCESS_KEY_ID 与 R2_SECRET_ACCESS_KEY 是凭据;其他值是连接和 URL 配置。若文件名可能复用,不要使用长期 immutable 缓存:应改为唯一文件名,或降低缓存时间。Cloudflare 的默认缓存资格和 TTL 会受对象类型、Cache Rules 等因素影响,因此“对象写入了 Cache-Control”不等价于“CDN 一定命中缓存”。Cache-Control concepts R2 custom-domain caching
3. 采用稳定的对象键约定#
博客图片使用:
{year}/{filename}{.suffix}例如本地 diagram.png 在 2026 年上传后是:
2026/diagram.png
https://cdn.example.com/2026/diagram.pngR2 中的 / 是 object key 的 prefix,不是传统目录;对象 key 不要以 / 开头,也不要包含 . 或 .. 路径段。建议文件名用有意义的、不可复用的名称(如带日期或内容短 hash),否则同名覆盖可能让浏览器和 CDN 在较长时间内返回旧字节。
4. 用本项目的上传工具#
本仓库提供 scripts/upload-r2-image.py,它不写入全局 AWS profile,而是在单次 aws 子进程中注入 R2 凭据。它会:
- 从文件名推导
YYYY/filename.ext;可用--year、--name显式覆盖。 - 推断或要求指定图片 MIME,并写入
Content-Type与Cache-Control。 - 先
head-object检查同名 key;默认拒绝覆盖,只有--overwrite才替换。 - 上传后再次读取 R2 metadata,并对 CDN URL 做
HEAD检查。 - 打印可直接粘贴的 Markdown,不打印凭据。
先检查本机配置和 token 的读权限:
./scripts/upload-r2-image.py doctor对一张图片做不触网的预演(无需真实 token):
./scripts/upload-r2-image.py upload /path/to/diagram.png \
--year 2026 \
--dry-run \
--env-file .env.example真实上传并获得 Markdown:
./scripts/upload-r2-image.py upload /path/to/diagram.png \
--alt "架构图"输出会包含类似以下内容:
若确实需要替换同一个 key,必须显式传入 --overwrite,并接受缓存仍可能暂时返回旧图的风险。更推荐上传新的文件名、修改 Markdown URL,而不是覆盖已有公开对象。
5. 排查顺序#
| 现象 | 优先检查 |
|---|---|
doctor 失败 | endpoint、bucket 名、R2 专用 token 的 bucket scope 与 Object Read & Write 权限;不要把一般 Cloudflare API token 当作 S3 凭据。 |
| 上传失败 | 本地文件、MIME、endpoint、token 权限;不要开启 AWS CLI --debug 后再把完整输出贴到公开处。 |
R2 成功但 CDN 404 | bucket 的 custom domain 是否已 Active、域名是否仍连接到该 bucket;不要用 r2.dev CNAME 代替。 |
| 页面显示为下载或类型错误 | 用 head-object 和 curl -I 检查 Content-Type;重新上传到新 key。 |
| 更新后仍是旧图 | 检查是否覆盖同名 key、浏览器缓存和 Cache Rules;换唯一 URL 是最可靠的修复。 |
发布前检查#
git check-ignore -v .env应显示.env被忽略;git status不应出现凭据文件。- 运行
doctor,再上传一个唯一 key 的非敏感图片。 - 保留工具输出的对象 key、CDN URL、MIME 和缓存头,确认 Markdown 使用的是 CDN URL。
- 只把可公开的图片放进 public bucket;R2 custom domain 可让任何人读取对应对象。