@ricenote/plugin-media
v1.3.0
Published
RiceNote 媒体库插件:上传、元数据与公开访问
Readme
rice.media —— 媒体库
文件上传、媒体元数据登记与公开访问。主题设置中的 image 字段、文章封面、
站点 Logo 保存的都是这里的媒体 ID 或访问地址。
清单摘要
| 项 | 值 |
| --- | --- |
| id | rice.media |
| version | 1.2.0 |
| apiVersion | ^1.0.0 |
| entry | ./dist/index.js |
| provides | media.library@1 |
| requires | 无 |
| capabilities | database:own、services:provide、services:consume、routes:admin、routes:public、media:store、extensions:declare、extensions:contribute |
| 扩展点 | rice.media.picker(admin,贡献 ./admin/picker.js) |
| migrations | 0001_media.sql |
| 数据表 | rn_media |
磁盘文件存放在受管目录
storage/uploads之下,数据库保存相对路径, 迁移部署目录时无需改数据。
扩展点(Hook)
原始路由(公开)
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | /media/:mediaId | 按 ID 反查落盘路径后回传文件;带长缓存与 nosniff |
SVG 额外加 Content-Security-Policy: default-src 'none'; …; sandbox,
避免同源上下文中以内联脚本的形式执行。
管理 API(权限点 media.manage)
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | /api/admin/media | 分页列表(可按文件名搜索) |
| POST | /api/admin/media | multipart 上传(站点设置里的 Logo/图标上传也走它) |
| DELETE | /api/admin/media/:mediaId | 删除记录与磁盘文件 |
服务契约(提供)
media.library@1:list / findById / save / remove / countAll。
save 的输入是 { fileName, mimeType, buffer, alt?, uploaderId? },
调用方(其他插件)不需要关心落盘路径。
服务契约(消费)
core.setup@1(必需):取 siteId() 与受管目录 paths().uploadsDir。
事件 / 插槽
无。
媒体选择器扩展点(rice.media.picker)
其他插件想在自己的界面里"选一张图 / 上传一个文件",不必自己写上传与列表 —— 渲染媒体库提供的面板即可(面板自带「上传新文件 / 从媒体库选择」两个页签, 上传与校验规则与媒体库页面完全相同):
<PluginSlot name="rice.media.picker" :context="{ onPick }">
<!-- 默认内容 = 媒体库不可用时的降级路径:一行直接填地址,功能不缺失 -->
<Input v-model="form.cover" placeholder="图片地址,例如 /media/12" />
</PluginSlot>function onPick({ url, fileName, mimeType, size }) {
form.cover = url;
}- 契约:唯一入参
onPick({ url, fileName, mimeType, size })。 不传onPick(例如放在只读预览里)时面板隐藏「插入」按钮,不会报错; - 替换语义:媒体库用
mode: replace(order: 0,permission: media.manage)贡献该面板 —— 媒体库启用时用完整面板替换消费者的默认内容;媒体库被停用 / 卸载时贡献随之消失, 消费者渲染默认内容。因此消费者必须给出"直接填链接"的降级路径; - 权限:贡献声明了
permission: media.manage,服务端按当前用户权限过滤; 无权限的用户收不到贡献,看到的就是默认内容; - 容器由消费者提供:面板不带外层对话框与标题,视觉沿用后台语言 (1px 细边框、无阴影、无毛玻璃)。
replaceable: false约束的是第三方插件:别的插件不能用replace接管这个面板。 提供方自己的replace不受该开关限制 —— 媒体库的面板就是该扩展点的"默认内容"本身, 若把自己也算进去,false会被逼成"谁都能替换我的面板",语义反而失真 (判据见packages/core/src/extensions/registry.ts的贡献校验)。
后台界面
界面文件位于 admin/main.js:零构建纯 ESM(不安装依赖、不打包,模板即字符串),
组件(AdminPage/Card/Button/Input/Icon…)已由宿主全局注册,
依赖只来自 /admin-sdk/v1/ricenote-admin.js。
菜单与入口由 manifest.json 的 admin.pages 声明式提供,Core 负责注册菜单、
托管界面文件并计算前端入口 URL(无需插件再调用 ctx.adminMenu.add):
{ id: 'media.library', label: '媒体库', path: '/media', icon: 'Image', order: 10, permission: 'media.manage' }
- 入口 URL:
/plugin-admin/rice.media/main.js - 访问权限:页面及列表 / 上传 / 删除均需
media.manage(上传的 multipart 字段名为file)
设置项(settingsSchema,后台自动生成表单)
后台「扩展中心 → 媒体库 → 设置」。保存后需重启服务生效。
分组「上传」
| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| maxFileBytes | number(1024–67108864) | 8388608(8MB) | 单文件大小上限(传输层上限见 MAX_UPLOAD_BYTES) |
插件侧会把该值再收敛到
1KB ~ 64MB,避免历史脏值让限制失效。
安全要点
- 上传文件名不可信:先
path.basename+ 白名单字符清洗,再随机化文件名; - 扩展名白名单:
png/jpg/jpeg/gif/webp/avif/svg/pdf/txt/md/zip; - MIME 由扩展名推断,不信任客户端声明;
- 落盘前归一化路径并断言仍在
uploads/内(防路径穿越)。
依赖与启停
- 可独立启停;
- 禁用后:
/media/:id与上传接口消失,media.library@1注销; posts 会降级为"按 ID 约定拼接/media/{id}"的访问方式(不报错); 后台站点设置的 Logo 上传会失败并提示改用外部地址。
