存储与文件
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 | 本地/私有部署 |
环境变量配置
在 .env.local 中添加以下变量:
| 变量名 | 说明 | 示例 |
|---|---|---|
S3_ENDPOINT | S3 服务的 API 地址 | https://cos.ap-guangzhou.myqcloud.com |
S3_REGION | 存储区域,R2/MinIO 填 auto | ap-guangzhou |
S3_ACCESS_KEY_ID | API 访问密钥 ID | AKIDxxxxxxxx |
S3_SECRET_ACCESS_KEY | API 访问密钥 Secret | xxxxxxxxxxxxxxxx |
S3_BUCKET | 存储桶名称 | my-app-public-1303088253 |
NEXT_PUBLIC_S3_ENDPOINT | 公共访问地址(客户端构建 URL 用) | https://my-app-public-1303088253.cos.ap-guangzhou.myqcloud.com |
NEXT_PUBLIC_S3_ENDPOINT 是浏览器可访问的完整域名,用于拼接文件的公共 URL。它和 S3_ENDPOINT(API 地址)通常不同——API 地址指向 COS 服务端点,公共地址指向具体的存储桶域名。
腾讯云 COS 配置
腾讯云 COS 是项目的默认存储目标。代码已内置兼容处理,会自动从 Bucket 名称提取 Appid 并注入请求头。
创建存储桶
登录 腾讯云 COS 控制台,创建一个存储桶。建议命名为 <项目名>-<用途>-<appid> 格式,例如 my-app-public-1303088253。
获取 API 密钥
进入 云 API 密钥管理,创建或查看 SecretId 和 SecretKey。
配置跨域(CORS)
在存储桶的「安全管理 > 跨域访问 CORS」中添加规则:
| 配置项 | 值 |
|---|---|
| 来源 Origin | http://localhost:7001(开发);生产环境填正式域名 |
| 允许 Methods | PUT, GET, HEAD |
| 允许 Headers | * |
| 暴露 Headers | ETag |
签名 URL 直传需要浏览器直接向 COS 发起 PUT 请求,必须配置 CORS,否则浏览器会拦截。
填写环境变量
S3_ENDPOINT=https://cos.ap-guangzhou.myqcloud.com
S3_REGION=ap-guangzhou
S3_ACCESS_KEY_ID=你的SecretId
S3_SECRET_ACCESS_KEY=你的SecretKey
S3_BUCKET=my-app-public-1303088253
NEXT_PUBLIC_S3_ENDPOINT=https://my-app-public-1303088253.cos.ap-guangzhou.myqcloud.comAWS S3 配置
在 AWS S3 控制台 创建存储桶,关闭「阻止所有公共访问」(如需公开读取)
配置 Bucket Policy(公开读取场景):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bucket-name/*"
}
]
}在 IAM 中创建用户,附加 s3:PutObject 和 s3:DeleteObject 权限,获取 Access Key
配置 CORS 并填写环境变量:
S3_ENDPOINT=https://s3.ap-southeast-1.amazonaws.com
S3_REGION=ap-southeast-1
S3_ACCESS_KEY_ID=你的AccessKeyId
S3_SECRET_ACCESS_KEY=你的SecretAccessKey
S3_BUCKET=your-bucket-name
NEXT_PUBLIC_S3_ENDPOINT=https://your-bucket-name.s3.ap-southeast-1.amazonaws.comCloudflare R2 配置
R2 兼容 S3 API 且无出口流量费用,适合高流量场景。
在 Cloudflare Dashboard > R2 Object Storage 中创建 Bucket
进入 R2 > Manage R2 API Tokens,创建 Token 并获取 Access Key ID 和 Secret Access Key
填写环境变量(Region 固定为 auto):
S3_ENDPOINT=https://<你的Account ID>.r2.cloudflarestorage.com
S3_REGION=auto
S3_ACCESS_KEY_ID=你的R2AccessKeyId
S3_SECRET_ACCESS_KEY=你的R2SecretAccessKey
S3_BUCKET=your-bucket-name
NEXT_PUBLIC_S3_ENDPOINT=https://your-custom-domain.comR2 需要绑定自定义域名或使用 R2.dev 公开访问域名作为 NEXT_PUBLIC_S3_ENDPOINT,它不直接提供 <bucket>.r2.cloudflarestorage.com 形式的公共 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 字段。