Halo 2 博客附件存储解耦实战:基于 S3 协议对接 Cloudflare R2 对象存储与无损迁移
在自建独立博客与内容管理系统(CMS)的过程中,服务器磁盘容量与带宽往往是最宝贵的资源。默认情况下,Halo 会将用户上传的文章图片、视频以及静态附件直接存放在宿主机的本地持久化目录(如 /opt/halo/data)。随着博客文章的积累,本地附件会带来一系列运维痛点:
- 备份体积臃肿:数据库备份仅有数兆,而包含海量高分辨率配图的附件目录会迅速膨胀到数 GB,极大拖慢每日定时异地备份的传输速度。
- 服务器带宽瓶颈:国内或轻量海外 VPS 的上行带宽普遍受限(通常为 3Mbps ~ 10Mbps)。当多位访客同时打开包含多张配图的技术文章时,往往瞬间占满服务器带宽,导致页面加载卡顿甚至服务器响应超时。
- 数据迁移成本高:在更换服务器或升级容器时,数十 GB 的文件传输不仅耗时,还容易因网络抖动出现断流或校验异常。
针对上述问题,业界标准的解决方案是**“动静分离与附件解耦”**——将网站的核心服务与附件存储剥离。本文将基于生产实战,手把手演示如何利用 Halo 的 S3 插件无缝对接 Cloudflare R2 对象存储,并完成存量静态配图的平滑迁移。
一、方案选型与技术优势
为什么选择 Cloudflare R2?
目前主流的对象存储服务包括 AWS S3、阿里云 OSS、腾讯云 COS、又拍云等。对比之下,Cloudflare R2 对个人开发者和小型站点有着无可替代的几大核心优势:
| 特性指标 | 传统对象存储(AWS / 国内大厂) | Cloudflare R2 |
|---|---|---|
| 免费存储额度 | 通常仅限新用户活动或无免费额度 | 每月 10 GB 永久免费存储空间 |
| API 调用额度 | 超额即计费 | 每月 100 万次写请求(Class A)+ 1000 万次读请求(Class B) 免费 |
| 出站流量费(Egress) | 昂贵(通常 0.5 ~ 0.8 元/GB,海外 AWS 更高) | $0.00 / GB(完全免出站流量费) |
| 全球 CDN 加速 | 需额外配置并支付 CDN 流量费 | 原生集成 Cloudflare Anycast 全球边缘缓存节点 |
| 协议兼容性 | 专有 API 或需额外适配 | 原生 100% 兼容 AWS S3 API |
对于个人独立博客而言,10GB 的免费额度足以容纳数万张高清配图;更重要的是,免除出站流量费彻底消除了因爬虫扫描、恶意盗链或网站流量突增而引发的“天价账单”风险。
二、架构拓扑与交互流程
解耦后的静态资源交互拓扑如下:
[ Halo 博客后台 / 编辑器 ]
│
▼ (1. S3 协议上传: PUT / Object Key)
[ Cloudflare R2 Storage (存储桶: halo) ]
│
▼ (2. 边缘缓存同步 / 回源拉取)
[ Cloudflare Anycast CDN 边缘缓存节点 ]
▲
│ (3. 客户端 HTTP GET 加速分发)
[ 终端访客浏览器 ]
- 写入阶段:当博主在后台上传附件或在 Markdown 编辑器中粘贴图片时,Halo 服务端通过 AWS S3 协议将文件直接流式写入 Cloudflare R2 存储桶。
- 读取阶段:文章渲染出的图片链接直接指向 Cloudflare 提供的公开域名或绑定的自定义域名;访客请求直接由离其地理位置最近的 Cloudflare 边缘节点响应,完全不消耗源站 VPS 的任何 CPU、内存与上行带宽。
三、第一步:Cloudflare R2 资源准备
在进行 Halo 配置之前,需在 Cloudflare 控制台创建存储桶并生成对应的 S3 访问凭证。
1. 创建 R2 存储桶
- 登录 Cloudflare Dashboard,在左侧导航栏点击 R2 对象存储。
- 点击 “创建存储桶(Create bucket)”。
- 输入存储桶名称(例如
halo),位置建议选择“自动(Automatic)”,点击确认创建。
2. 生成 S3 兼容 API 令牌
R2 原生支持 S3 客户端接入,但需要专属的密钥对:
- 在 R2 概览页面的右侧,点击 “管理 R2 API 令牌(Manage R2 API Tokens)”。
- 点击 “创建 API 令牌”:
- 权限:选择 “对象读和写(Object Read & Write)”。
- TTL:根据需要选择永久或指定过期时间。
- 客户端 IP 限制:如源站 VPS 为固定公网 IP,可配置白名单进一步提升安全性;留空则不限 IP。
- 创建成功后,立即记录保存以下四项核心参数(界面关闭后 Secret 将无法再次查看):
- 访问密钥 ID(Access Key ID):例如
a5fa2477aea4ba55309462c2d1349462 - 机密访问密钥(Secret Access Key):例如
897f37652c8d1af8e94b... - 管辖权地特定终结点(S3 Endpoint):格式形如
https://<ACCOUNT_ID>.r2.cloudflarestorage.com
3. 配置公共访问域(Public Access)
新创建的存储桶默认对公网私有。为了让博客访客能正常访问图片,需要开启公共访问:
- 进入该存储桶详情页 -> 设置(Settings) -> 公共访问(Public Access)。
- 方案 A(快速测试):开启 R2.dev 公共开发 URL(例如
https://pub-xxxxxx.r2.dev)。 - 方案 B(生产推荐):点击 自定义域(Custom Domains),绑定你已在 Cloudflare 解析的二级域名(如
assets.yourdomain.com),不仅 URL 更简洁,还能直接继承主域名的缓存规则与 WAF 防护。
四、第二步:Halo 插件选型与安装
Halo 2 采用了微内核与插件化架构,内置仅包含本地存储策略。对接 R2 需要安装官方维护的 S3 对象存储插件(plugin-s3)。
关键避坑点:Halo 核心版本与插件兼容性
Halo 2 处于高速迭代中,plugin-s3 插件对 Halo 底层核心版本有着严格的约束。若盲目安装 GitHub 上的 Latest 版本,会导致插件无法启动:
plugin-s3 v1.17.x➡️ 要求 Halo>= 2.24.0plugin-s3 v1.14.x➡️ 要求 Halo>= 2.22.0plugin-s3 v1.12.0➡️ 要求 Halo>= 2.20.0(适合 Halo 2.20.x 长期稳定版)
请根据你的 Halo 版本下载对应版本的 JAR 包(以 Halo 2.20 为例):
# 下载兼容当前 Halo 2.20 的 S3 插件
curl -sL "https://github.com/halo-dev/plugin-s3/releases/download/v1.12.0/plugin-s3-1.12.0.jar" \
-o /opt/halo/data/plugins/plugin-s3-1.12.0.jar
chmod 644 /opt/halo/data/plugins/plugin-s3-1.12.0.jar
进入 Halo 管理后台 -> 应用市场 / 插件,或直接通过 Console API 安装并启用该插件。
五、第三步:配置 S3 存储策略
插件启用后,Halo 的存储策略模板中将新增 “S3 对象存储(s3os)”。
在 Halo 后台导航至 【附件】->【存储策略】->【新建存储策略】,选择 S3 对象存储,参考下表精确填入各项配置:
| 配置项 | 填写规范与示例 | 注意事项(踩坑高发区) |
|---|---|---|
| 策略名称 | Cloudflare R2 |
仅作为后台显示的策略标识 |
| Bucket 桶名称 | halo |
必须与 Cloudflare 中创建的存储桶名完全一致 |
| Endpoint 访问协议 | HTTPS |
默认选项 |
| Endpoint | <ACCOUNT_ID>.r2.cloudflarestorage.com |
⚠️ 严禁带有 https:// 前缀,结尾不要带斜杠 / |
| Endpoint 访问风格 | Virtual Hosted Style 或 Path Style |
R2 对两种风格均完美兼容,保持默认即可 |
| Access Key ID | a5fa2477aea4ba55309462c2d1349462 |
粘贴 R2 令牌页面的访问密钥 |
| Access Key Secret | 897f37652c8d1af8e... |
粘贴 R2 令牌页面的机密访问密钥 |
| Region(区域) | auto |
⚠️ 致命陷阱:必须填写全小写英文字母 auto!如果留空,SDK 默认使用 us-east-1,会直接导致 R2 抛出 400 校验错误 |
| 绑定域名(CDN 域名) | pub-xxxxxx.r2.dev 或 assets.yourdomain.com |
协议选 HTTPS,此处填纯域名(不要带 https://) |
| 上传目录 | 留空 或 ${year}/${month} |
留空则默认保存在桶根目录 |
| 上传重命名方式 | 保留原文件名 或 使用UUID |
按个人偏好选择 |
点击保存后,插件会自动发起一次对象存储探测验证。验证通过即代表后端与 Cloudflare R2 通信完全通畅。
将 R2 设为全站默认存储策略
进入后台 【系统设置】->【基本设置】:
- 文章附件策略:切换为 Cloudflare R2。
- 用户中心附件/头像策略:切换为 Cloudflare R2。
配置完成后,此后在 Halo 撰写文章时粘贴或上传的任何图片,都会直接写入 Cloudflare R2。
六、第四步:历史存量附件平滑迁移
如果你的博客此前已经运行了一段时间,文章中通常引用了存放在本地文件系统(如主题静态目录或默认本地策略)中的旧图片。我们需要完成以下两步实现全站无感切换:
1. 批量同步本地图片至 R2
你可以使用 Halo 的 REST API 进行同步,也可以直接利用通用的 S3 CLI 工具(如 aws-cli 或 rclone)将本地图片一键同步到 R2 存储桶中:
# 以 rclone 或 curl 配合 S3 API 为例,将本地图片推送到 R2
# 同步完成后在浏览器中验证外链:
curl -sI "https://pub-xxxxxx.r2.dev/jdcloud-turboacc-nss.jpg"
# 正常应返回 HTTP/1.1 200 OK
2. 文章 Markdown 图片链接批量替换
通过 Halo 的 OpenAPI,可以编写自动化脚本遍历已发布的文章,将旧的相对路径或本地路径自动批量替换为 R2 的加速链接:
import json, urllib.request, markdown
token = "YOUR_HALO_PAT_TOKEN"
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
post_id = "post-xgojwycd"
# 1. 读取文章当前元数据与 Raw Markdown
post_obj = json.loads(urllib.request.urlopen(
urllib.request.Request(f"http://127.0.0.1:8090/apis/content.halo.run/v1alpha1/posts/{post_id}", headers=headers)
).read().decode())
content_obj = json.loads(urllib.request.urlopen(
urllib.request.Request(f"http://127.0.0.1:8090/apis/api.console.halo.run/v1alpha1/posts/{post_id}/head-content", headers=headers)
).read().decode())
# 2. 执行路径替换
raw_md = content_obj.get("raw", "")
raw_md = raw_md.replace(
"/themes/theme-Stack2/assets/images/",
"https://pub-xxxxxx.r2.dev/"
)
# 3. 重新渲染 HTML 并提交更新
html_md = markdown.markdown(raw_md, extensions=['fenced_code', 'tables'])
payload = {
"post": post_obj,
"content": {"raw": raw_md, "content": html_md, "rawType": "markdown"}
}
put_req = urllib.request.Request(
f"http://127.0.0.1:8090/apis/api.console.halo.run/v1alpha1/posts/{post_id}",
data=json.dumps(payload).encode(),
headers=headers,
method="PUT"
)
urllib.request.urlopen(put_req)
# 4. 触发发布接口使快照生效 (Reconciliation)
pub_req = urllib.request.Request(
f"http://127.0.0.1:8090/apis/api.console.halo.run/v1alpha1/posts/{post_id}/publish",
data=b"{}",
headers=headers,
method="PUT"
)
urllib.request.urlopen(pub_req)
print("文章快照更新并重新发布成功!")
七、验证与性能测试
访问已发布的技术文章,打开浏览器开发者工具(F12)的 Network(网络) 标签页刷新页面:
- 资源地址校验:检查图片请求的 Host,已由原本的博客域名平滑切换为
pub-xxxxxx.r2.dev或自定义图片域。 - 响应头检查:
http HTTP/1.1 200 OK Content-Type: image/jpeg Server: cloudflare CF-RAY: 8...-SJC CF-Cache-Status: HIT当CF-Cache-Status呈现HIT时,表明请求已被离访客最近的 Cloudflare CDN 边缘节点直接拦截并高速返回,完全免除了对云存储的读取计费,也彻底解放了源站服务器的宝贵带宽。
结语
通过为 Halo 2 接入 Cloudflare R2 对象存储,我们在享受 0 流量费、10GB 免费空间与全球边缘 CDN 加速 的同时,实现了博客架构与静态数据的彻底解耦。无论是日常的高清图文排版,还是未来的整站容器迁移与灾备恢复,都将变得更加轻盈、稳定和从容。