# PluginCnbStorage **Repository Path**: uniomo/PluginCnbStorage ## Basic Information - **Project Name**: PluginCnbStorage - **Description**: No description available - **Primary Language**: Unknown - **License**: GPL-3.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-07 - **Last Updated**: 2026-10-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # PluginCnbStorage · CNB 静态资源存储(Halo 插件) > 将 Halo 附件(图片 / 视频 / 压缩包 / 文档等)上传到 **CNB.cool**,使用 CNB 作为静态资源 CDN,不再占用本地存储。 | 项目 | 说明 | | --- | --- | | 插件名称 | `PluginCnbStorage` | | 显示名 | CNB 静态资源存储 | | 版本 | `0.2.0` | | 适用 Halo | `>= 2.26.0`(开发基于 2.26.1) | | 作者 | 雨落秋垣 · | | 源码仓库 | | | 问题反馈 | | | 许可证 | [GPL-3.0](./LICENSE) | --- ## 1. 功能特性 - **附件直传 CNB**:在 Halo 后台「存储策略」中选择 CNB 策略后,新上传的附件(图片 / 视频 / 压缩包 / 文档等)直接写入 CNB 公开仓库,并回写公开访问 URL,本地不再落盘。 - **存量附件一键迁移**:提供控制台内迁移页面(SSE 实时进度)与后端 API,把已有的本地附件批量搬到 CNB,并**自动改写**文章 / 页面 / 图库 / 分类封面以及快照正文里的旧图片链接。 - **安全兜底**:迁移只处理**图片 / 视频 / 音频**类静态资源;**绝不迁移 JS / CSS / JSON**(否则会破坏站点页面与脚本加载)。 - **防 404 设计**:针对 Halo 图片优化导致的文件名改写(追加随机后缀、扩展名变化),采用「基准名索引」做模糊匹配替换,消除迁移后旧链接残留 404 的问题。 - **可回退清理**:支持「迁移成功后删除本地原文件」与独立的 `purge-local` 批量清理接口;本地文件**仅在内容链接替换成功之后**才删除,避免链接未替换就删文件导致访问失败。 - **连接自检**:存储策略表单内置「CNB 连接验证」,保存前即校验令牌与仓库可读性。 --- ## 2. 系统架构与类职责 ``` ┌──────────────────────── Halo 附件体系 ────────────────────────┐ │ │ 上传请求 ──► CnbAttachmentHandler (实现 AttachmentHandler 扩展点) │ │ upload / delete / getPermalink / getSharedURL │ │ getThumbnailLinks / shouldHandle / buildAttachment │ ▼ │ CnbClient (CNB Open API WebClient 封装) │ │ init(POST /-/upload/files) → PUT uploadUrl → confirm │ │ delete / testConnection │ ▼ │ CNB.cool 公开仓库(静态资源 CDN) │ │ 迁移请求 ──► CnbMigrationController (REST: /apis/cnb.halo.run/v1alpha1/...) │ │ /migrate /migrate/stream(SSE) /migrate/purge-local │ │ /migrate/ui /policies/cnb/validation │ ▼ │ CnbAttachmentMigrator (@Component 核心迁移引擎) │ │ list Attachment → upload → rewriteContent → deleteLocal │ └────────────────────────────────────────────────────────────────┘ ``` | 类 | 职责 | | --- | --- | | `CnbStoragePlugin` | 插件入口,`@Component extends BasePlugin`,`start()`/`stop()` 为空实现。 | | `CnbAttachmentHandler` | 实现 `AttachmentHandler` 扩展点。`shouldHandle` 判断策略 `templateName == "cnb"`;`upload` 读取策略配置并把文件字节上传到 CNB;`buildAttachment` 写入注解 `cnb.plugin.halo.run/path` 与 `storage.halo.run/external-link`。 | | `CnbClient` | 基于 Spring `WebClient` 的 CNB Open API 客户端。上传三步:① `POST {api}/{repo}/-/upload/files`(拿 `uploadUrl`/`token`/`assets.path`)② `PUT uploadUrl` 推送字节 ③ `PUT {api}/{repo}/-/files/{path}` 确认。`delete` / `testConnection`(先传一个测试文件再删)亦在此实现。 | | `CnbPolicySetting` | 策略配置模型(`apiEndpoint` / `webEndpoint` / `repo` / `token`),从策略 ConfigMap 的 `data.default` JSON 反序列化;`toPermalink` / `toApiUrl` / `normalizeStoragePath` 负责地址拼装与清洗。 | | `CnbInitResponse` | CNB 上传初始化响应的 DTO(`uploadUrl` / `token` / `assets.path`)。 | | `CnbApiException` | 携带 `HttpStatusCode` 的 CNB API 异常类型。 | | `CnbMigrationController` | 迁移相关 REST 控制器(见 §9、§10)。 | | `CnbPolicyValidationController` | 存储策略表单「连接验证」后端:`POST /apis/cnb.halo.run/v1alpha1/policies/cnb/validation`。 | | `CnbAttachmentMigrator` | **核心迁移引擎**(见 §4)。负责列举本地附件、并发上传、构建链接映射、改写内容、后置清理。 | | `CnbPhoto` | 本地定义的 `Photo` 扩展子类型(`@GVK group=core.halo.run`),用于改写图库封面链接;以 `Map` 形式存取 `spec`,规避全局 Scheme 未注册的报错。 | | `MigrateRequest` / `MigrateResult` / `MigrateEvent` | 迁移请求 / 结果 / SSE 事件的 DTO。 | --- ## 3. 工作原理 ### 3.1 附件上传流程 1. 用户在 Halo 附件上传接口选择 CNB 存储策略。 2. `CnbAttachmentHandler.upload` 校验策略模板名为 `cnb`,从策略 `ConfigMap` 解析出 `CnbPolicySetting`。 3. 读取上传文件字节,调用 `CnbClient.upload`: - `POST {apiEndpoint}/{repo}/-/upload/files`,头 `Authorization: Bearer `,体 `{ "name", "size" }`; - 取得 `uploadUrl` 后 `PUT` 字节流(Content-Type 取原媒体类型); - 再 `PUT {apiEndpoint}/{repo}/-/files/{path}` 确认,拿到 CNB 资源路径。 4. `buildAttachment` 写回附件:随机 UUID 命名、`displayName`/媒体类型/大小,并打注解 `cnb.plugin.halo.run/path`(CNB 路径)与 `storage.halo.run/external-link`(公开 URL);`status.permalink` 设为 CNB 公开地址。 > 公开地址格式:`{webEndpoint}/{repo}/-/files/{path}`,例如 `https://cnb.cool///-/files/2026/01/15/xxx.png`。 ### 3.2 存量附件迁移流程(`CnbAttachmentMigrator`) ``` migrate(req) └─ resolvePolicy(name 或 displayName) # 按元数据名或显示名定位策略 └─ fetch ConfigMap → CnbPolicySetting └─ runMigration ├─ list Attachment (谓词: 含 storage.halo.run/local-relative-path 且为允许的媒体) ├─ migrateOne: 读本地字节 → cnbClient.upload → applyCnb(写注解/permalink) → update ├─ 汇总本次成功映射 buildUrlMap(old→new) ├─ collectCnbUrlMap: 把历史已迁移(含以往运行)的 CNB 附件也并入映射 ├─ expandKeys: 展开 日期子目录 / 扁平文件名 / URL 编码 多种等价形态 ├─ 构建 baseIndex(基准名 → CNB URL) ├─ rewriteContent: 改写 Snapshot(contentPatch+rawPatch)/Post 封面/Page 封面/Photo 封面/Category 封面 └─ 仅当 rewriteContent 成功 且 deleteLocal=true → 删除本地原文件 ``` **关键安全顺序**:先「改写内容链接」、成功后再「删除本地文件」。即便删除阶段出错,公开 URL 已生效,不会出现旧链接 404。 ### 3.3 内容链接替换与 404 根因修复(重点) **问题根因**:Halo 的图片优化会在上传后**重命名磁盘文件**(追加 4 位随机后缀,如 `image-Jisi-feKF`)并**改写扩展名**(如 `.png` → `.webp`)。因此正文 / 封面里引用的旧链接(原始文件名)与 CNB 上实际文件名(带优化后缀、新扩展名)不一致,若只做“精确字符串替换”,旧链接会残留 → 浏览器 404。 **修复方案**(`replaceUrls` 两步 + `baseIndex` + `expandKeys`): 1. **精确替换**:对每条 `旧URL→新URL` 映射,依次替换 - 绝对域名形态:`https?://`; - 相对形态:``; - URL 编码形态及其小写态(覆盖 `中文名` / 空格编码等写法)。 2. **基准名容错替换**:用 `BASE_URL_PATTERN` 扫描内容中的 `/upload/...` 路径,取「基准名」(`baseNameOf`:去扩展名、再去 Halo 优化随机后缀 `-XXXX`)查 `baseIndex`(基准名 → CNB URL),命中即整段替换为 CNB 链接。 3. **形态展开**(`expandKeys`):把每条映射额外展开为「`/upload/日期/子/目录/文件名`」「`/upload/文件名`(扁平)」「URL 编码态」等多种等价键,覆盖正文不同写法。 此外,`applyCnb` 会记录 `cnb.plugin.halo.run/origin-url`(迁移前原始本地访问地址),`collectCnbUrlMap` 用它兜底重建 `/upload/`,覆盖早期迁移未记录该注解的历史附件。 ### 3.4 媒体白名单(绝不迁移 JS / CSS) `isAllowedMediaType` 只允许 图片(`image/*`) / 视频(`video/*`) / 音频(`audio/*)` 以及 `ALLOWED_EXT` 中列出的扩展名(png/jpg/gif/webp/svg/bmp/ico/avif/heic/tif/mp4/webm/mov/avi/mkv/mp3/wav/ogg/flac/aac…)。明确拒绝 `text/css`、`*javascript*`、`application/json`。**切勿修改此白名单去包含脚本/样式**——把 JS/CSS 搬到 CDN 会破坏页面与后台脚本加载。 --- ## 4. 环境要求 - 一台已安装 **Halo >= 2.26.0** 的服务器(本插件开发基于 2.26.1,JDK 21)。 - 一个 **CNB.cool** 公开仓库(用于存放附件),以及具备该仓库读写权限的**访问令牌**。 - 服务器可访问 `https://api.cnb.cool` 与 `https://cnb.cool`。 --- ## 5. 安装与启用 本仓库根目录已附**预编译部署包** `PluginCnbStorage-0.2.0.jar`,直接部署即可: 1. 停止 Halo(或确认插件目录可热加载)。 2. 将 `PluginCnbStorage-0.2.0.jar` 复制到 Halo 的插件目录(默认 `~/.halo2/plugins/`,或你自定义的 `halo.plugin.dir`)。 3. 启动 Halo,在「控制台 → 插件」中确认 `PluginCnbStorage` 状态为 **已启用 (STARTED)**。 4. 如从旧版本升级,建议先在「插件」页停用旧版本再放新 jar,避免文件名锁定冲突。 > 本地验证时若提示「另一个程序正在使用此文件」,说明 Halo 正在运行并锁定 jar,需先停 Halo 再覆盖。 --- ## 6. CNB 存储策略配置 1. 控制台 → 「附件 → 存储策略 → 添加策略」,模板选择 **CNB 静态资源存储**(模板名 `cnb`)。 2. 填写表单(对应 `extensions/policy-template-cnb.yaml`): | 字段 | 说明 | 默认值 | | --- | --- | --- | | CNB API 地址 | Open API 基址 | `https://api.cnb.cool` | | CNB Web 地址 | 用于拼公开访问地址 | `https://cnb.cool` | | 仓库路径 | 存放附件的公开仓库,格式 `组织名/仓库名` | 必填 | | 访问令牌 Token | CNB 个人设置 → 访问令牌,**需包含 `repo-contents:rw` 权限** | 必填 | 3. 点击「CNB 连接验证」按钮,通过即表示令牌与仓库可读写。 4. 保存策略;新上传的附件即走 CNB。 > **令牌安全**:令牌写在策略 `ConfigMap` 中,仅服务端使用,不在迁移页重复填写。请为专用仓库签发最小权限令牌,并妥善保管。 --- ## 7. 存量附件迁移 迁移会把「已有本地附件」搬到 CNB,并改写内容里的旧链接。**强烈建议先 dry-run 预览**,确认无误后再正式执行并(可选)删除本地文件。 ### 7.1 控制台内迁移页面 访问 `GET /apis/cnb.halo.run/v1alpha1/migrate/ui`(需登录控制台),打开迁移页: - **目标 CNB 存储策略名称**(必填):例如 `cnb-default`,也支持填显示名(如 `uniomo.com`)。 - **来源策略名称**(可选):留空=迁移所有本地附件;填写则只迁移该来源策略下的附件。 - **本地附件根目录**(可选):留空自动取 `halo.work-dir/attachments`,否则取 `~/.halo2/attachments`。 - **并发上传数**(1–32,默认 4)、**失败重试次数**(0–10,默认 2)。 - **仅预览 (dry-run)**:只统计、不上传、不删文件。 - **成功后删除本地文件**:勾选后,内容链接替换成功才删除本地原文件。 - **批量删除本地原文件**:单独调用 `purge-local`,只删本地、不上传。 页面通过 SSE 实时显示总数 / 成功 / 失败 / 跳过 / 预览 与迁移体积,并逐条列出结果。 ### 7.2 使用 SSE 流式接口(命令行) ```bash # 先登录控制台拿到 SESSION Cookie,保存到 session.txt(仅裸 UUID) curl -s --noproxy '*' -c cookie.txt -X POST \ -H 'Content-Type: application/json' \ -d '{"username":"<账号>","password":"<密码>"}' \ http://localhost:8090/api/v1alpha1/users/-/login # dry-run 预览 curl -s --noproxy '*' -N -b cookie.txt \ "http://localhost:8090/apis/cnb.halo.run/v1alpha1/migrate/stream?targetPolicyName=cnb-default&dryRun=true" ``` ### 7.3 批量清理本地原文件 ```bash curl -s --noproxy '*' -b cookie.txt -X POST \ -H 'Content-Type: application/json' \ -d '{"targetPolicyName":"cnb-default"}' \ http://localhost:8090/apis/cnb.halo.run/v1alpha1/migrate/purge-local ``` ### 7.4 迁移参数速查 | 参数 | 来源 | 必填 | 说明 | | --- | --- | --- | --- | | `targetPolicyName` | query / body | 是 | 目标 CNB 策略(名或显示名) | | `sourcePolicyName` | query / body | 否 | 仅迁移该来源策略下的附件 | | `localRoot` | query / body | 否 | 本地附件根目录 | | `dryRun` | query / body | 否 | 默认 `false`,仅预览 | | `deleteLocal` | query / body | 否 | 默认 `false`,替换成功后删本地 | | `concurrency` | query / body | 否 | 默认 `4`,上限 `32` | | `maxRetries` | query / body | 否 | 默认 `2`,单附件失败重试 | --- ## 8. API 参考 基址:`/apis/cnb.halo.run/v1alpha1` | 方法 | 路径 | 说明 | | --- | --- | --- | | `POST` | `/migrate` | 非流式迁移,返回 `MigrateResult` JSON。 | | `GET` | `/migrate/stream` ( `text/event-stream` ) | SSE 流式迁移,事件类型 `start` / `item` / `summary`。 | | `POST` | `/migrate/purge-local` | 批量删除「已迁移附件」的本地原文件。 | | `GET` | `/migrate/ui` | 返回内置迁移页面 HTML(`cnb-migration.html`)。 | | `POST` | `/policies/cnb/validation` | 存储策略表单「连接验证」后端。 | 注解约定(附件 metadata.annotations): - `cnb.plugin.halo.run/path`:CNB 资源路径。 - `cnb.plugin.halo.run/origin-local-path`:迁移前本地相对路径(清理用)。 - `cnb.plugin.halo.run/origin-url`:迁移前原始本地访问地址(链接替换兜底用)。 - `storage.halo.run/external-link`:CNB 公开访问 URL。 --- ## 9. 本地编译打包 > 本仓库快照采用**手工编译**方式产出 jar(环境内无 Gradle/Maven)。下方给出可复现的手工步骤;正式开源协作推荐改用 Halo 官方 Gradle 插件模板(见「后续计划」)。 ### 9.1 手工编译(javac + jar) 前提:本机已运行或可取得 Halo 2.26.x 的应用依赖(提供 `run.halo.app.*`、`org.pf4j`、`reactor`、`spring-web`、`jackson` 等 API)。 ```bash # 1) 编译 Java 源到 out/ javac -proc:none -d out \ -cp "" \ src/run/halo/cnb/*.java # 2) 把资源打进 jar(plugin.yaml / META-INF / extensions / ui / cnb-migration.html) jar cfm PluginCnbStorage-0.2.0.jar META-INF/MANIFEST.MF \ -C out . \ plugin.yaml META-INF extensions ui cnb-migration.html ``` - ``:Halo 发行包的依赖目录(如 `halo.jar` 及其 `lib/`)。本仓库 `src/` 下的 `.java` 已包含 lombok 生成的访问器,编译时无需 lombok 处理器(故用 `-proc:none`)。 - `plugin.yaml` 的 `logo` 字段为 **data URI 内联 PNG**:因为 Halo 不把插件 `ui/` 目录作为静态资源服务(相对路径 `ui/logo.png` 在控制台返回 404),故将 logo 直接内联进 manifest,随 jar 携带、任意 `` 均可渲染。源文件 `ui/logo.png`(512×512)保留为设计素材。 ### 9.2 目录结构 ``` PluginCnbStorage/ (本仓库根 = C:\Users\Administrator\Desktop\slg) ├── PluginCnbStorage-0.2.0.jar # 预编译部署包 ├── README.md # 本文档 ├── LICENSE # GPL-3.0 全文 ├── plugin.yaml # 插件清单(含 data-URI logo) ├── cnb-migration.html # 内置迁移页面(SSE 前端) ├── META-INF/ │ ├── MANIFEST.MF │ └── plugin-components.idx # Halo 生成的组件索引 ├── extensions/ │ └── policy-template-cnb.yaml # CNB 存储策略模板 + 设置表单 ├── src/run/halo/cnb/ # Java 源码(见 §2) │ ├── CnbStoragePlugin.java │ ├── CnbAttachmentHandler.java │ ├── CnbClient.java │ ├── CnbPolicySetting.java │ ├── CnbInitResponse.java │ ├── CnbApiException.java │ ├── CnbMigrationController.java │ ├── CnbPolicyValidationController.java │ ├── CnbAttachmentMigrator.java # 核心迁移引擎(含 404 修复) │ ├── CnbPhoto.java │ ├── MigrateRequest.java │ ├── MigrateResult.java │ └── MigrateEvent.java └── ui/ # 预构建前端资源 ├── ui-plugin.json ├── main.O61ZB7nM.js ├── assets/MigrationView.IbtjqO4f.css ├── chunks/MigrationView.Dz4SeDrd.js └── logo.png # 插件 logo 设计素材(512×512) ``` --- ## 10. 已知限制与后续计划 - **前端为预构建产物**:`ui/` 下是已构建的 JS/CSS,本快照未包含前端源码与构建链路;如需二次开发 UI 需自行接入 Halo 前端工程。 - **Photo 类型兜底**:部分 Halo 实例未在本体 Scheme 注册 `Photo` 扩展,`rewritePhotos` 已做 `try/catch` 兜底,不会中断文章 / 分类等其他链接的改写。 - **构建链路**:计划补回 Halo 官方 Gradle 插件模板(`build.gradle` + `settings.gradle`),标准化依赖与打包,替代手工 `javac`。 - **大文件 / 断点续传**:当前为单次全量上传;超大队列建议适当调低 `concurrency` 并保留 `maxRetries`。 --- ## 11. 许可证 本项目以 **GNU General Public License v3.0 (GPL-3.0)** 发布。详见 [LICENSE](./LICENSE)。 > 版权所有 (c) 2026 雨落秋垣 。 > 本程序为自由软件,以「原样」提供,不附带任何担保。 --- ## 12. 链接 - 插件市场 / 源码: - 问题反馈: - Halo 文档: - CNB.cool: