npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 上传会失败并提示改用外部地址。