- 后端
- 前端
- 云原生
【免费下载链接】hedgedoc
HedgeDoc - Ideas grow better together
导读
HedgeDoc 将笔记中上传的图片(Media)统一交由可插拔的存储后端处理,Imgur 是官方支持的五个后端之一。本文围绕 docs/content/references/config/media/imgur.md 这份配置参考,完整讲解如何注册 Imgur 应用、获取 Client ID、通过环境变量启用 Imgur 后端,并结合 backend/src/media/backends/imgur-backend.ts 与 backend/src/config/media.config.ts 等源码,深入说明上传、删除、URL 解析的实现机制与数据存储结构。读完本文,你将能独立完成 HedgeDoc 的 Imgur 后端配置,并理解其背后的工作原理与数据留存风险。
一、Imgur 在 HedgeDoc 媒体架构中的角色
HedgeDoc 的媒体(Media)指与笔记关联的上传文件,目前仅支持图片。媒体文件可以保存在本地文件系统、S3、Azure Blob、通用 WebDAV 共享目录,以及 Imgur 上(见 docs/content/concepts/media.md)。
每个存储后端都必须实现统一的 MediaBackend 接口,共三个方法:
saveFile(uuid, buffer, fileType):存储给定文件,可返回一段字符串化的元数据交给数据库保存,元数据格式由后端自行定义,仅供后端内部使用;deleteFile(uuid, metadata):根据 UUID 与元数据删除文件;getFileUrl(uuid, metadata):返回文件的公开访问 URL,该 URL 可以是临时的。
Imgur 后端(ImgurBackend)正是按此接口实现的。启用它之后,HedgeDoc 前端上传图片时,MediaService会调用该后端将图片二进制数据转发给 Imgur 的匿名上传接口,并把 Imgur 返回的链接与删除令牌记录到数据库(详见 backend/src/media/media.service.ts)。
需要特别说明:Imgur 作为托管在公网的第三方图床,上传的图片会以 Imgur 的域名对外提供访问,而非由 HedgeDoc 自身托管。适合希望将图片存储从自有服务器剥离、接受图片托管在第三方平台的部署场景。
二、重要提醒:匿名图片只有 6 个月存留期
!!! warning "Imgur 仅保存匿名图片 6 个月" Imgur 会在最后一次访问 6 个月后,删除所有未关联到任何账号的图片。 这意味着如果你使用 Imgur 作为图片后端,你或你的用户上传的图片可能会丢失。
这是官方配置参考文档开篇即给出的警告:当 Imgur 应用以“Anonymous usage without user authorization”(无需用户授权的匿名使用)方式创建时,所有上传的图片都不关联任何 Imgur 账号,Imgur 将在最后一次被访问的 6 个月后将其删除。官方文档进一步指向了 Imgur 帮助中心的 FAQ 条目作为依据。
这一风险意味着:
- 长期存档的笔记中的图片可能在 6 个月后 404;
- 如果笔记被持续访问,图片的“最后一次访问”时间会被刷新,存留期会顺延;
- 对于需要长期可靠保存图片的部署,应优先评估文件系统、S3 等自有后端;
- 若只是演示、开发或短期协作场景,Imgur 的零运维特性则很有吸引力。
配置前请务必与团队确认这一数据留存策略是否可接受。
三、注册 Imgur 应用并获取 Client ID
启用 Imgur 后端所需的全部准备工作,就是注册一个 Imgur 应用并拿到Client ID。官方配置参考给出了如下步骤:
- 在 imgur.com 注册一个账号并登录;
- 打开 Imgur 的 OAuth2 应用注册页面
https://api.imgur.com/oauth2/addclient; - 填写表单,并将授权类型(Authorization type)选择为“Anonymous usage without user authorization”(匿名使用,无需用户授权);
- 提交后,Imgur 会显示你的 Client ID。
这里强调两点:
- 必须选择匿名使用模式。HedgeDoc 的 Imgur 后端在上传时仅携带
Client-ID请求头(见下文的源码分析),并不执行 OAuth 用户授权流程,因此应用必须按匿名模式创建; - Client ID 不同于 Client Secret。配置中只需要 Client ID,不要填入任何密钥类信息。
四、在 HedgeDoc 中启用 Imgur 后端
4.1 配置项一览
拿到 Client ID 后,在 HedgeDoc 的配置中加入以下两行(将<IMGUR_CLIENT_ID>替换为你的真实 Client ID):
HD_MEDIA_BACKEND_TYPE="imgur" HD_MEDIA_BACKEND_IMGUR_CLIENT_ID="<IMGUR_CLIENT_ID>"其中:
| 环境变量 | 说明 | 取值 |
|---|---|---|
HD_MEDIA_BACKEND_TYPE | 选择媒体存储后端类型 | 固定为imgur |
HD_MEDIA_BACKEND_IMGUR_CLIENT_ID | Imgur 应用的 Client ID | 从 Imgur 应用注册页获取的字符串 |
4.2 配置文件的放置位置
HedgeDoc 的配置完全由环境变量驱动,所有变量以HD_前缀开头。NestJS 会从进程环境以及项目根目录的.env文件中读取这些变量(见 docs/content/concepts/config.md)。在官方 Docker 容器中,.env的位置是/usr/src/app/.env(见 docs/content/references/config/index.md)。
一个同时包含数据库与媒体配置的最小.env示例大致如下:
HD_BASE_URL="http://localhost:8080" HD_SESSION_SECRET="change_me_in_production" HD_DATABASE_TYPE="sqlite" HD_DATABASE_NAME="./hedgedoc.sqlite" HD_MEDIA_BACKEND_TYPE="imgur" HD_MEDIA_BACKEND_IMGUR_CLIENT_ID="your_client_id_here"注意:此示例仅为展示媒体配置的书写方式,.env.example中提供的是用于本地开发的最小配置,不可未经修改直接用于生产。
4.3 配置如何被校验
媒体配置在 backend/src/config/media.config.ts 中定义。其中 Imgur 相关的 schema 如下:
const imgurSchema = z.object({ type: z.literal(MediaBackendType.IMGUR), imgur: z.object({ clientId: z.string().describe('HD_MEDIA_BACKEND_IMGUR_CLIENT_ID'), }), });也就是说:
type必须严格等于imgur(MediaBackendType.IMGUR),传入其他值将导致配置校验失败;clientId必须是一个字符串,缺失时校验会报错,并在启动时通过printConfigErrorAndExit打印包含该环境变量名的错误信息后退出进程(见 backend/src/config/utils.ts 引用的 utils 与 backend/src/config/zod-error-message.ts)。
除了后端选择之外,该配置文件还定义了上传大小上限HD_MEDIA_MAX_UPLOAD_SIZE,默认值为 20 MB(20 * 1024 * 1024),同样作用于 Imgur 后端。若需调整可一并配置:
HD_MEDIA_MAX_UPLOAD_SIZE="52428800"五、源码级原理:ImgurBackend 的工作机制
5.1 类结构与条件实例化
Imgur 后端的实现位于 backend/src/media/backends/imgur-backend.ts。ImgurBackend通过构造函数注入媒体配置,并仅在HD_MEDIA_BACKEND_TYPE=imgur时才真正读取imgur.clientId:
constructor( private readonly logger: ConsoleLoggerService, @Inject(mediaConfiguration.KEY) private mediaConfig: MediaConfig, ) { this.logger.setContext(ImgurBackend.name); // only create the backend if imgur is configured if (this.mediaConfig.backend.type !== MediaBackendType.IMGUR) { return; } this.config = this.mediaConfig.backend.imgur; }MediaService在启动时根据配置的backend.type通过getBackendFromType选择对应后端实例,Imgur 对应的分支返回moduleRef.get(ImgurBackend)(见 backend/src/media/media.service.ts)。
5.2 上传:向 Imgur API 提交 base64 图片
saveFile方法把上传文件的二进制内容转为 base64,通过 POST 请求发送到 Imgur 的匿名上传接口:
async saveFile(uuid: string, buffer: Buffer): Promise<string> { const params = new URLSearchParams(); params.append('image', buffer.toString('base64')); params.append('type', 'base64'); const result = (await fetch('https://api.imgur.com/3/image', { method: 'POST', body: params, headers: { Authorization: `Client-ID ${this.config.clientId}` }, }) .then((res) => ImgurBackend.checkStatus(res)) .then((res) => res.json())) as UploadResult; const backendData: ImgurBackendData = { url: result.data.link, deleteHash: result.data.deletehash, }; return JSON.stringify(backendData); }关键细节:
- 请求头
Authorization: Client-ID <clientId>正是前文“无需用户授权”模式的体现——它只携带应用标识,不携带用户令牌; - Imgur 的响应包含
data.link(图片公开 URL)和data.deletehash(删除令牌,匿名模式下可据此删除图片,可能为null); - 这两个字段被组装成
{ url, deleteHash }结构并JSON 序列化后返回,作为backendData存入数据库; - 请求失败(HTTP 非 2xx)会抛出
MediaBackendError,最终以Could not save file <uuid>的形式报错。
5.3 访问:返回重定向 URL
由于 Imgur 上的图片本身就是公开 URL,getFileUrl不需要代理字节流,只需从数据库中保存的元数据解析出 url 并返回:
getFileUrl(uuid: string, backendData: string | null): Promise<string> { const data = JSON.parse(backendData) as ImgurBackendData; return Promise.resolve(data.url); }在 backend/src/media/media.service.ts 的getFileResponse中,非文件系统后端统一走redirect分支——即访问媒体接口时,HedgeDoc 返回 302 跳转到 Imgur 的图片地址,而不是把图片字节流经自己服务器中转。
5.4 删除:借助 deleteHash 调用匿名删除接口
deleteFile会先解析元数据;若deleteHash为null(比如 Imgur 未返回删除令牌),则直接抛出MediaBackendError,说明无法删除该图片:
async deleteFile(uuid: string, jsonBackendData: string): Promise<void> { const backendData = JSON.parse(jsonBackendData) as ImgurBackendData; if (backendData.deleteHash === null) { throw new MediaBackendError( `We don't have any delete tokens for file ${uuid} and therefore can't delete this image`, ); } const result = await fetch(`https://api.imgur.com/3/image/${backendData.deleteHash}`, { method: 'DELETE', headers: { Authorization: `Client-ID ${this.config.clientId}` }, }); ImgurBackend.checkStatus(result); }可见:HedgeDoc 的“删除图片”最终依赖 Imgur 的匿名删除令牌(delete hash)接口。这也再次印证了文档中“backendData列中保存的是删除令牌”的表述——它是后续删除操作的唯一凭证,一旦丢失便无法再从 Imgur 侧删除图片。
5.5 上传链路中的 MIME 校验
在进入后端之前,MediaService.saveFile会先调用 commons/src/file-type/extract-file-type.ts 检测文件类型,并与白名单比对,不匹配的直接拒绝(MIME type not allowed.)。白名单覆盖image/png、image/jpeg、image/gif、image/webp、image/svg+xml、image/tiff、image/bmp、image/heic等常见图片格式(见 backend/src/media/media.service.ts 中的isAllowedMimeType)。也就是说,只有检测为图片的文件才会被转发给 Imgur。
六、上传记录:media_uploads 表与 backendData
文档明确指出:“所有上传都会保存在media_uploads数据库表中,并在backendData列中包含删除令牌”。这与源码完全吻合:
- 数据库类型定义位于 database/src/types/media-upload.ts,
TableMediaUpload = 'media_upload',关键字段包括:uuid:媒体上传的公开唯一标识(UUID v7);user_id:上传者;file_name:原始文件名;backend_type:存储后端类型,Imgur 场景下为imgur;backend_data:后端所需的附加元数据——对 Imgur 而言正是{"url":"...","deleteHash":"..."}的 JSON 字符串;created_at:上传时间;
- 上传事务(见
MediaService.saveFile)会同时向media_upload表插入记录、向media_upload_note表建立媒体与笔记的关联; - 删除操作(
MediaService.deleteFile)先按uuid查询backend_data,交给后端删除,成功后再从media_upload表移除记录。
因此,运维人员可以直接在数据库中查看某张图片的 Imgur 地址与删除令牌,便于排查问题或手工清理。
七、注意事项小结
- 数据存留:匿名模式上传的图片在最后一次访问 6 个月后会被 Imgur 删除,长期内容请谨慎选择此后端;
- 删除能力:图片可删性依赖
backendData中的deleteHash,缺失时 HedgeDoc 会明确报错并保留记录; - 上传限制:默认最大上传 20 MB,可通过
HD_MEDIA_MAX_UPLOAD_SIZE调整,且仅允许白名单内的图片 MIME 类型; - 配置错误定位:若
HD_MEDIA_BACKEND_TYPE不是imgur或HD_MEDIA_BACKEND_IMGUR_CLIENT_ID缺失,HedgeDoc 启动时会打印包含具体环境变量名的错误信息并退出,便于快速修正; - 参考实现:Imgur 后端的完整行为可在 backend/src/media/backends/imgur-backend.ts 中查看,与其他后端(S3、Azure、WebDAV、文件系统)并列于 backend/src/media/backends/ 目录,方便对比各后端的差异。
总体而言,Imgur 后端是 HedgeDoc 中配置成本最低的图片存储方案之一——只需一个 Client ID、两行环境变量即可启用。但 6 个月的匿名存留期使其更适合短期协作与演示场景;对图片有长期留存要求的部署,建议在配置前评估文件系统、S3 或 WebDAV 等自有后端方案。
- 后端
- 前端
- 云原生
【免费下载链接】hedgedoc
HedgeDoc - Ideas grow better together
相关推荐
Neon compute 镜像怎么构建:扩展补丁、neon 扩展与 vm-image-spec 的作用
Neon compute 镜像怎么构建:扩展补丁、neon 扩展与 vm image spec 的作用 当你需要给 compute 节点加一个扩展补丁、验证某个
后端前端云原生3步搞定Amlogic盒子无线难题:RTL8822CS网卡驱动完全指南
3步搞定Amlogic盒子无线难题:RTL8822CS网卡驱动完全指南 你是否曾为Amlogic S9xxx设备刷入Armbian系统后无线网卡无法使用而烦恼?
嵌入式开发工具构建工具操作系统使用 rage 作为 chezmoi 的加密后端:从配置到源码级原理
使用 rage 作为 chezmoi 的加密后端:从配置到源码级原理 导读 chezmoi 默认使用 age https://link.gitcode.com/
开发工具CLI配置管理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考