集成
00 / 00

存储与文件

S3 兼容存储配置、文件上传、图片水印和公共 URL 构建指南

概览

项目使用 S3 兼容对象存储(Object Storage,一种通过网络接口读写文件的存储服务)来管理所有文件资源。核心能力包括:

  • 签名 URL 直传 — 浏览器通过服务器签发的临时 URL 直接上传到 S3,无需中转,节省带宽
  • 服务端上传 — 服务器直接将文件写入 S3,适合后台处理场景
  • 图片水印 — 基于 sharp 给图片添加可配置的 Logo 水印
  • 公共 URL 构建 — 将存储路径转换为可访问的完整 URL

相关代码位于 packages/storage/,客户端上传辅助函数位于 packages/storage/src/client.ts

支持的存储服务

任何兼容 S3 协议的服务均可使用:

服务Endpoint 示例说明
腾讯云 COShttps://cos.ap-guangzhou.myqcloud.com国内对象存储可选项,已内置 Appid 注入兼容
AWS S3https://s3.ap-southeast-1.amazonaws.com标准 S3
Cloudflare R2https://<account-id>.r2.cloudflarestorage.com推荐做公共图片资产仓库
MinIOhttp://localhost:9000本地/私有部署

Muse 推荐对象存储方案

当前默认推荐用 Cloudflare R2 做公共图片资产仓库:

R2 bucket: muse-public-assets
公开域名: https://assets.example.com
管理后台: https://r2.example.com

写文档时,Markdown/MDX 中使用稳定的自定义域名:

![demo](https://assets.example.com/images/muse/2026/05/demo.png)

R2 的优势是 S3 API 兼容、成本低、配置简单,并且可以用自定义域名提供公开访问。模板代码通过 S3 兼容接口访问对象存储,因此后续可以切换到 AWS S3、腾讯云 COS、MinIO 或其他兼容服务,不需要重写业务逻辑。

日常管理推荐用 R2 Web,它支持中文、拖拽上传、粘贴上传、图片本地压缩和复制 Markdown 链接。详细配置见 Cloudflare R2 接入指南

R2 适合文档图片、文章截图、封面图和公开附件。面向中国大陆稳定分发、OTA、App 安装包或课程大文件时,应单独设计国内 OSS/CDN 回源链路。

环境变量配置

products/muse/packages/config/.env 中添加以下变量:

变量名说明示例
PUBLIC_UPLOAD_ENDPOINTS3/R2 API 地址https://<account-id>.r2.cloudflarestorage.com
PUBLIC_UPLOAD_REGION存储区域,R2/MinIO 填 autoauto
PUBLIC_UPLOAD_ACCESS_KEY_IDAPI 访问密钥 IDAKIDxxxxxxxx
PUBLIC_UPLOAD_SECRET_ACCESS_KEYAPI 访问密钥 Secretxxxxxxxxxxxxxxxx
PUBLIC_UPLOAD_BUCKET存储桶名称muse-public-assets
PUBLIC_UPLOAD_PUBLIC_URL文件公开访问基础 URLhttps://assets.example.com

API endpoint 和公开 URL 解决的是两件事,不要混用。

变量给谁用作用是否公开
PUBLIC_UPLOAD_ENDPOINT服务端、上传接口、脚本连接 S3 兼容 API,用来上传、删除、签名 URL 和管理对象不作为浏览器变量公开
PUBLIC_UPLOAD_PUBLIC_URL服务端生成公开 URL、Markdown/MDX 图片、公开下载链接拼接已经上传文件的公开 URL可以公开访问

PUBLIC_UPLOAD_ENDPOINT 本身不是密钥,但写入能力来自 PUBLIC_UPLOAD_ACCESS_KEY_IDPUBLIC_UPLOAD_SECRET_ACCESS_KEY。这组写入凭证只能放在服务端环境变量、受保护的管理工具或本机插件里。

如果文件不是公开资料,不要用 PUBLIC_UPLOAD_PUBLIC_URL 直接拼公开链接;改用后端鉴权或有时效的签名 URL。

上传方式

签名 URL 直传(推荐)

浏览器先从服务器获取一个有时效性的签名 URL(Signature URL),然后直接用 PUT 请求将文件上传到 S3。文件不经过服务器,节省带宽和服务器资源。

// 前端调用示例
import { uploadWithSignedUrlFallback } from "@repo/storage/client";

const publicUrl = await uploadWithSignedUrlFallback({
  file,              // File 对象
  bucket: "my-bucket",
  path: `uploads/${Date.now()}-${file.name}`,
  contentType: file.type,
  publicEndpoint: process.env.PUBLIC_UPLOAD_PUBLIC_URL,
});
// publicUrl 即上传后文件的可访问地址

uploadWithSignedUrlFallback 会自动处理签名 URL 失败的情况:如果签名 URL 上传失败,会自动回退到服务端中转上传,无需手动处理。

服务端中转上传

适合需要在上传前做服务端处理(如图片压缩、水印、病毒扫描)的场景。服务端收到文件后通过 uploadFileToS3 写入 S3。

import { uploadFileToS3 } from "@repo/storage";

await uploadFileToS3("path/to/file.jpg", {
  bucket: process.env.PUBLIC_UPLOAD_BUCKET!,
  body: fileBuffer,
  contentType: "image/jpeg",
});

文件删除

import { deleteFileFromS3 } from "@repo/storage";

await deleteFileFromS3("path/to/file.jpg", {
  bucket: process.env.S3_BUCKET!,
});

图片水印

项目内置基于 sharp 的水印工具,可在图片上叠加 Logo 水印。Logo 文件路径默认为 public/images/logo-white.png

import { addWatermark, isLogoAvailable } from "@repo/storage";

// 检查 Logo 文件是否可用
if (await isLogoAvailable()) {
  const watermarked = await addWatermark(imageBuffer, {
    position: "bottom-right", // top-left | top-right | bottom-left | bottom-right
    opacity: 0.7,             // 0-1,透明度
    logoSize: 600,            // Logo 宽度(像素),默认 600
  });
  // watermarked 是处理后的图片 Buffer,可直接上传到 S3
}

水印功能依赖 sharp 库,仅在服务端可用,不要在客户端组件中调用。

公共 URL 构建

数据库中存储的是相对路径(如 uploads/abc.jpg),显示时需要拼接为完整 URL。@repo/storage 提供了工具函数:

import { getPublicStorageUrl, mapPublicStorageUrls } from "@repo/storage";

// 单个路径
const url = getPublicStorageUrl("uploads/abc.jpg", process.env.PUBLIC_UPLOAD_PUBLIC_URL);
// => "https://your-bucket.cos.ap-guangzhou.myqcloud.com/uploads/abc.jpg"

// 已是完整 URL 的值会原样返回
const full = getPublicStorageUrl("https://example.com/img.jpg", process.env.PUBLIC_UPLOAD_PUBLIC_URL);
// => "https://example.com/img.jpg"

// 批量转换对象中的多个字段
const updated = mapPublicStorageUrls(organization, ["logo", "coverImage"], process.env.PUBLIC_UPLOAD_PUBLIC_URL);

项目还预置了 withOrganizationPublicUrls,自动转换组织对象中的 logocoverImageaudienceQrCodememberQrCode 字段。

常见问题

各存储服务接入指南

想和其他创造者交流?

这篇文档有问题?