存储与文件
S3 兼容存储配置、文件上传、图片水印和公共 URL 构建指南
概览
项目使用 S3 兼容对象存储(Object Storage,一种通过网络接口读写文件的存储服务)来管理所有文件资源。核心能力包括:
- 签名 URL 直传 — 浏览器通过服务器签发的临时 URL 直接上传到 S3,无需中转,节省带宽
- 服务端上传 — 服务器直接将文件写入 S3,适合后台处理场景
- 图片水印 — 基于
sharp给图片添加可配置的 Logo 水印 - 公共 URL 构建 — 将存储路径转换为可访问的完整 URL
相关代码位于 packages/storage/,客户端上传辅助函数位于 packages/storage/src/client.ts。
支持的存储服务
任何兼容 S3 协议的服务均可使用:
| 服务 | Endpoint 示例 | 说明 |
|---|---|---|
| 腾讯云 COS | https://cos.ap-guangzhou.myqcloud.com | 项目默认目标,已内置 Appid 注入兼容 |
| AWS S3 | https://s3.ap-southeast-1.amazonaws.com | 标准 S3 |
| Cloudflare R2 | https://<account-id>.r2.cloudflarestorage.com | 推荐做公共图片资产仓库 |
| MinIO | http://localhost:9000 | 本地/私有部署 |
01MVP 推荐图床方案
当前推荐用 Cloudflare R2 做公共图片资产仓库:
R2 bucket: 01mvp-public-assets
公开域名: https://assets.01mvp.com
管理后台: https://r2.01mvp.com写文档时,Markdown/MDX 中使用稳定的自定义域名:
日常管理推荐用 R2 Web,它支持中文、拖拽上传、粘贴上传、图片本地压缩和复制 Markdown 链接。详细配置见 Cloudflare R2 接入指南。
R2 适合文档图片、文章截图、封面图和公开附件。面向中国大陆稳定分发、OTA、App 安装包或课程大文件时,应单独设计国内 OSS/CDN 回源链路。
环境变量配置
在 .env.local 中添加以下变量:
| 变量名 | 说明 | 示例 |
|---|---|---|
S3_ENDPOINT | S3 服务的 API 地址 | https://<account-id>.r2.cloudflarestorage.com |
S3_REGION | 存储区域,R2/MinIO 填 auto | auto |
S3_ACCESS_KEY_ID | API 访问密钥 ID | AKIDxxxxxxxx |
S3_SECRET_ACCESS_KEY | API 访问密钥 Secret | xxxxxxxxxxxxxxxx |
S3_BUCKET | 存储桶名称 | 01mvp-public-assets |
NEXT_PUBLIC_S3_ENDPOINT | 公共访问地址(客户端构建 URL 用) | https://assets.01mvp.com |
S3_ENDPOINT 和 NEXT_PUBLIC_S3_ENDPOINT 解决的是两件事,不要混用。
| 变量 | 给谁用 | 作用 | 是否公开 |
|---|---|---|---|
S3_ENDPOINT | 服务端、上传接口、脚本 | 连接 S3 兼容 API,用来上传、删除、签名 URL 和管理对象 | 不作为浏览器变量公开 |
NEXT_PUBLIC_S3_ENDPOINT | 浏览器页面、Markdown/MDX 图片、公开下载链接 | 拼接已经上传文件的公开 URL | 可以公开访问 |
S3_ENDPOINT 本身不是密钥,但写入能力来自 S3_ACCESS_KEY_ID 和 S3_SECRET_ACCESS_KEY。这组写入凭证只能放在服务端环境变量、受保护的管理工具或本机插件里,不要用 NEXT_PUBLIC_ 暴露给前端。
如果文件不是公开资料,不要用 NEXT_PUBLIC_S3_ENDPOINT 直接拼公开链接;改用后端鉴权或有时效的签名 URL。
上传方式
签名 URL 直传(推荐)
浏览器先从服务器获取一个有时效性的签名 URL(Signature URL),然后直接用 PUT 请求将文件上传到 S3。文件不经过服务器,节省带宽和服务器资源。
// 前端调用示例
import { uploadWithSignedUrlFallback } from "@01mvp/storage/client";
const publicUrl = await uploadWithSignedUrlFallback({
file, // File 对象
bucket: "my-bucket",
path: `uploads/${Date.now()}-${file.name}`,
contentType: file.type,
publicEndpoint: process.env.NEXT_PUBLIC_S3_ENDPOINT,
});
// publicUrl 即上传后文件的可访问地址uploadWithSignedUrlFallback 会自动处理签名 URL 失败的情况:如果签名 URL 上传失败,会自动回退到服务端中转上传,无需手动处理。
服务端中转上传
适合需要在上传前做服务端处理(如图片压缩、水印、病毒扫描)的场景。服务端收到文件后通过 uploadFileToS3 写入 S3。
import { uploadFileToS3 } from "@01mvp/storage";
await uploadFileToS3("path/to/file.jpg", {
bucket: process.env.S3_BUCKET!,
body: fileBuffer,
contentType: "image/jpeg",
});文件删除
import { deleteFileFromS3 } from "@01mvp/storage";
await deleteFileFromS3("path/to/file.jpg", {
bucket: process.env.S3_BUCKET!,
});图片水印
项目内置基于 sharp 的水印工具,可在图片上叠加 Logo 水印。Logo 文件路径默认为 public/images/logo-white.png。
import { addWatermark, isLogoAvailable } from "@01mvp/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。@01mvp/storage 提供了工具函数:
import { getPublicStorageUrl, mapPublicStorageUrls } from "@01mvp/storage";
// 单个路径
const url = getPublicStorageUrl("uploads/abc.jpg", process.env.NEXT_PUBLIC_S3_ENDPOINT);
// => "https://your-bucket.cos.ap-guangzhou.myqcloud.com/uploads/abc.jpg"
// 已是完整 URL 的值会原样返回
const full = getPublicStorageUrl("https://example.com/img.jpg", process.env.NEXT_PUBLIC_S3_ENDPOINT);
// => "https://example.com/img.jpg"
// 批量转换对象中的多个字段
const updated = mapPublicStorageUrls(organization, ["logo", "coverImage"], process.env.NEXT_PUBLIC_S3_ENDPOINT);项目还预置了 withOrganizationPublicUrls,自动转换组织对象中的 logo、coverImage、audienceQrCode、memberQrCode 字段。