存储与文件

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

01MVP 推荐图床方案

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

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

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

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

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

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

环境变量配置

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

变量名说明示例
S3_ENDPOINTS3 服务的 API 地址https://<account-id>.r2.cloudflarestorage.com
S3_REGION存储区域,R2/MinIO 填 autoauto
S3_ACCESS_KEY_IDAPI 访问密钥 IDAKIDxxxxxxxx
S3_SECRET_ACCESS_KEYAPI 访问密钥 Secretxxxxxxxxxxxxxxxx
S3_BUCKET存储桶名称01mvp-public-assets
NEXT_PUBLIC_S3_ENDPOINT公共访问地址(客户端构建 URL 用)https://assets.01mvp.com

S3_ENDPOINTNEXT_PUBLIC_S3_ENDPOINT 解决的是两件事,不要混用。

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

S3_ENDPOINT 本身不是密钥,但写入能力来自 S3_ACCESS_KEY_IDS3_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,自动转换组织对象中的 logocoverImageaudienceQrCodememberQrCode 字段。

常见问题

各存储服务接入指南