Cloudflare R2 图片上传到 Hugo 博客:安全、可验证的 AWS CLI 流程

This article is extracted from the chat log with AI. Please identify it with caution.

说明:本文由 Codex 根据作者提供的主题、对话素材与 Cloudflare 官方文档辅助生成,属于 AI 生成/整理内容,非作者原创。请读者自行甄别并交叉验证。

**结论:把博客图片上传到 R2 最稳妥的方式,是使用仅限目标 bucket 的 R2 专用 Object Read & Write token,通过 S3 兼容 endpoint 写入 YYYY/filename.ext,再用已启用的自定义 CDN 域名访问。**上传工具应显式写入 Content-TypeCache-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 支持 PutObjectContent-TypeCache-Control 等系统元数据;因此无需依赖文件扩展名猜测浏览器响应头。S3 API compatibility aws CLI 示例

生产访问使用 R2 bucket 的自定义域名,而不是 r2.devr2.dev 是开发用途且受限;自定义域名需要位于同一 Cloudflare 账户的 zone 中,并在 bucket 的 Settings → Custom Domains 显示为 Active。R2 的 S3 ACL 不提供 public-read,因此不要给 AWS CLI 加 --acl public-readPublic buckets and custom domains

1. 创建最小权限的 R2 凭据#

在 Cloudflare Dashboard 的 R2 Object Storage → Manage API Tokens 创建 R2 专用 API token

  1. 权限选择 Object Read & Write
  2. 仅 scope 到博客使用的目标 bucket。
  3. 保存页面显示的 Access Key IDSecret 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_IDR2_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.png

R2 中的 / 是 object key 的 prefix,不是传统目录;对象 key 不要以 / 开头,也不要包含 ... 路径段。建议文件名用有意义的、不可复用的名称(如带日期或内容短 hash),否则同名覆盖可能让浏览器和 CDN 在较长时间内返回旧字节。

4. 用本项目的上传工具#

本仓库提供 scripts/upload-r2-image.py,它不写入全局 AWS profile,而是在单次 aws 子进程中注入 R2 凭据。它会:

  • 从文件名推导 YYYY/filename.ext;可用 --year--name 显式覆盖。
  • 推断或要求指定图片 MIME,并写入 Content-TypeCache-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 "架构图"

输出会包含类似以下内容:

![架构图](https://cdn.example.com/2026/diagram.png)

若确实需要替换同一个 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 404bucket 的 custom domain 是否已 Active、域名是否仍连接到该 bucket;不要用 r2.dev CNAME 代替。
页面显示为下载或类型错误head-objectcurl -I 检查 Content-Type;重新上传到新 key。
更新后仍是旧图检查是否覆盖同名 key、浏览器缓存和 Cache Rules;换唯一 URL 是最可靠的修复。

发布前检查#

  1. git check-ignore -v .env 应显示 .env 被忽略;git status 不应出现凭据文件。
  2. 运行 doctor,再上传一个唯一 key 的非敏感图片。
  3. 保留工具输出的对象 key、CDN URL、MIME 和缓存头,确认 Markdown 使用的是 CDN URL。
  4. 只把可公开的图片放进 public bucket;R2 custom domain 可让任何人读取对应对象。

参考资料#

本文共 2063 字,创建于 Aug 7, 2026

相关标签: Cloud, DevOps, ByAI