功能指南

存储与文件

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本地/私有部署

环境变量配置

.env.local 中添加以下变量:

变量名说明示例
S3_ENDPOINTS3 服务的 API 地址https://cos.ap-guangzhou.myqcloud.com
S3_REGION存储区域,R2/MinIO 填 autoap-guangzhou
S3_ACCESS_KEY_IDAPI 访问密钥 IDAKIDxxxxxxxx
S3_SECRET_ACCESS_KEYAPI 访问密钥 Secretxxxxxxxxxxxxxxxx
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」中添加规则:

配置项
来源 Originhttp://localhost:7001(开发);生产环境填正式域名
允许 MethodsPUT, GET, HEAD
允许 Headers*
暴露 HeadersETag

签名 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.com

AWS 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:PutObjects3: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.com

Cloudflare 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.com

R2 需要绑定自定义域名或使用 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,自动转换组织对象中的 logocoverImageaudienceQrCodememberQrCode 字段。

常见问题

相关资源