← SaaS 全栈知识库

📁 文件存储

S3 / Cloudflare R2 / MinIO / CDN — 对象存储选型、图片处理、大文件上传、安全策略
目录 1. 什么是文件存储?为什么独立选型? 2. 对象存储服务商对比 3. AWS S3 深度解析 4. Cloudflare R2 — 零出口费的破局者 5. MinIO — 自托管 S3 替代 6. 上传架构:直传 vs 服务端中转 7. 大文件:分片上传、断点续传 8. 图片处理:Resize / 格式转换 / CDN 9. CDN 策略:缓存、回源、预热 10. 安全:签名 URL、CORS、加密 11. 成本模型与优化 12. 选型决策树 13. 实战代码示例 14. 替代方案与特殊场景 15. 关联:Agent 研究中的文件处理

1. 什么是文件存储?为什么独立选型?

文件存储是 SaaS 产品中用户生成内容 (UGC) 的归宿——头像、文档、图片、视频、导出文件。不同于数据库存结构化数据,文件存储解决的是:

📦 对象存储

文件作为「对象」存入「桶 (Bucket)」,每个对象有唯一 Key。无目录层级限制,无限扩展。典型:S3、R2、MinIO。

💿 块/文件存储

EBS、EFS、NFS——挂载到服务器,像本地磁盘一样用。适合临时文件、日志,不适合 UGC(无 CDN、无签名 URL、扩展性差)。

🧠 核心原则:UGC 永远放对象存储,不放服务器磁盘。原因——
① 服务器是无状态的,随时可能扩缩容/重启
② 对象存储天然 CDN 友好,全球加速
③ 签名 URL 实现细粒度权限控制
④ 按用量付费,不需要预分配磁盘

存储类型全景

类型代表适用场景CDN签名 URL
对象存储S3, R2, MinIOUGC、静态资源、备份
块存储EBS, 云硬盘数据库数据目录、临时文件
文件存储EFS, NFS共享配置、日志聚合
CDN 边缘存储R2, KV, D1全球低延迟读取✅ 内置

2. 对象存储服务商对比

服务商存储费/GB/月出口费请求费CDNS3 兼容自托管
AWS S3$0.023$0.09/GB$0.0005/1KCloudFront 另计
Cloudflare R2$0.015$0 (零出口费)$0.36/M内置 (Free)
MinIO自备硬件自备带宽需自建
Google GCS$0.020$0.12/GB$0.005/10KCloud CDN 另计
Azure Blob$0.018$0.087/GB$0.005/10KAzure CDN 另计
Backblaze B2$0.006$0.01/GBFree ≤2500/天Cloudflare 联盟
Wasabi$0.0069$0$0需自建
Tencent COS¥0.118¥0.50/GB¥0.01/万CDN 回源免费
Alibaba OSS¥0.120¥0.50/GB¥0.01/万CDN 回源免费
💰 出口费是最大陷阱:S3 存储 1TB 数据仅需 $23.5/月,但如果用户下载 500GB,出口费高达 $45。R2 零出口费 = 读密集型场景省 60-80%

按场景推荐

🌍 全球 SaaS(出海产品)

R2 > S3+CloudFront > Backblaze B2

零出口费 + 内置 CDN 是杀手锏。S3 生态最全但出口费恐怖。

🇨🇳 国内 SaaS

腾讯 COS / 阿里 OSS > 七牛 > 又拍云

国内 CDN 回源免费,备案域名加速。R2 在国内访问慢。

🔒 数据敏感 / 合规

MinIO 自托管 > S3 (GovCloud) > R2

医疗、金融数据不出机房。MinIO 完全掌控。

🚀 早期 MVP / 低预算

Backblaze B2 > R2 > Wasabi

B2 $0.006/GB + Cloudflare 联盟免费 CDN = 极低起步。

3. AWS S3 深度解析

S3 是对象存储的事实标准,几乎所有竞品都兼容 S3 API。理解 S3 就是理解整个对象存储生态。

核心概念

概念说明类比
Bucket顶层命名空间,全局唯一名称磁盘分区
Object实际存储的文件 + 元数据文件
Key对象在桶内的唯一标识 (含前缀)文件路径
Region桶所在的 AWS 区域数据中心
Storage Class存储层级:Standard / IA / Glacier热/温/冷盘
Versioning同一 Key 多版本共存Git 历史

存储层级策略

🟢 Standard
$0.023/GB
→ 30天 🟡 Standard-IA
$0.0125/GB
→ 90天 🟠 Glacier Instant
$0.004/GB
→ 180天 🔴 Glacier Deep Archive
$0.00099/GB

生命周期规则自动迁移,零代码:

# S3 Lifecycle Configuration
{
  "Rules": [{
    "ID": "auto-tier",
    "Status": "Enabled",
    "Transitions": [
      { "Days": 30, "StorageClass": "STANDARD_IA" },
      { "Days": 90, "StorageClass": "GLACIER_INSTANT" }
    ],
    "Expiration": { "Days": 365 }
  }]
}

S3 的隐性成本

⚠️ 出口费是最大支出
S3 出口费 $0.09/GB = 下载 1TB 数据要付 $92。如果你的 SaaS 允许用户下载文件(导出、图片查看),出口费可能超过存储费 5-10 倍

优化策略:① 用 CloudFront CDN 缓存(出口费降至 $0.02-0.14/GB 取决于量)② 迁移到 R2(零出口费)③ 用 S3 Transfer Acceleration(全球加速但更贵)

何时选 S3

4. Cloudflare R2 — 零出口费的破局者

R2 是 Cloudflare 推出的 S3 兼容对象存储,杀手锏 = 零出口费。基于 Cloudflare 全球网络,内置 CDN。

R2 的经济学

场景S3 + CloudFrontR2节省
1TB 存储 + 500GB 下载/月$23.5 + $45 = $68.5$15 + $0 = $1578%
1TB 存储 + 5TB 下载/月$23.5 + $450 = $473.5$15 + $0 = $1597%
100GB 存储 + 1TB 下载/月$2.35 + $90 = $92.35$1.5 + $0 = $1.598%

R2 核心 API 兼容性

R2 实现 S3 API 子集,可用 AWS SDK 直接访问:

// 用 AWS SDK v3 连接 R2
import { S3Client } from '@aws-sdk/client-s3';

const r2 = new S3Client({
  region: 'auto',
  endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY_ID,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
  },
});

// 之后所有 S3 操作照常使用
// PutObject, GetObject, DeleteObject, ListObjectsV2 ...

R2 独有特性

R2 的限制

⚠️ R2 当前限制
① 单对象最大 5TB(和 S3 一致),但分片上传 Part 数限制 10,000
不支持 S3 事件通知(没有 → Lambda/SQS 触发),需用 Worker 绑定轮询
不支持 S3 Select(SQL 查询 CSV/JSON)
④ 不支持版本控制的完整 API(仅保留最新版本)
⑤ 国内访问延迟高(Cloudflare 在国内节点少)

R2 + Worker 图片处理

// Cloudflare Worker: 图片 Resize On-the-fly
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const key = url.pathname.slice(1); // /avatars/123.jpg → avatars/123.jpg
    const size = url.searchParams.get('w') || '200';

    // 从 R2 取原图
    const obj = await env.MY_BUCKET.get(key);
    if (!obj) return new Response('Not Found', { status: 404 });

    // 用 Cloudflare Image Resizing (需开通)
    const imageRequest = new Request(
      `https://cdn.example.com/cdn-cgi/image/width=${size}/${key}`
    );
    return fetch(imageRequest);
  }
};

5. MinIO — 自托管 S3 替代

MinIO 是用 Go 写的高性能自托管对象存储,100% S3 API 兼容。单二进制文件,启动即用。

为什么自托管?

✅ 适合自托管

  • 数据合规(金融/医疗数据不出机房)
  • 超大规模存储(PB 级,公有云太贵)
  • 边缘/离线场景(工厂、船只)
  • 开发/测试环境(零成本模拟 S3)

❌ 不适合自托管

  • 需要全球 CDN 加速
  • 团队无运维能力
  • 数据量小(< 1TB)
  • 需要高可用 99.99%+

MinIO 部署(Docker Compose)

# docker-compose.yml — MinIO 单节点 + Console
version: "3.8"
services:
  minio:
    image: minio/minio:latest
    ports:
      - "9000:9000"    # S3 API
      - "9001:9001"    # Console
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin
    command: server /data --console-address ":9001"
    volumes:
      - minio-data:/data

volumes:
  minio-data:

MinIO 高可用:纠删码

MinIO 用纠删码 (Erasure Coding) 而非副本实现数据冗余。4 块盘可以丢 2 块,16 块盘可以丢 8 块——比 3 副本节省 50% 存储空间。

# 分布式 MinIO — 4 节点 x 4 盘 = 16 盘纠删码
MINIO_VOLUMES="https://node{1...4}.example.com/mnt/disk{1...4}"
# 数据条带: 8 数据 + 8 校验 = 可丢 8 盘

MinIO 性能

在标准硬件上,MinIO 可达 325 GiB/s GET / 165 GiB/s PUT(32 节点集群)。单节点 NVMe 可轻松 10Gbps+。对于 SaaS 应用,单节点 MinIO 足够支撑 10 万+ QPS。

6. 上传架构:直传 vs 服务端中转

这是文件存储设计中最关键的架构决策。

方案对比

维度客户端直传服务端中转
文件流Client → S3Client → Server → S3
服务器负载双倍带宽
权限控制Pre-signed URL / STS服务端鉴权后转发
文件大小限制5TB (S3)受服务器内存/超时限制
实现复杂度中等(需前端 SDK)简单
安全审查需预校验(类型/大小)服务端完全控制

推荐:预签名 URL 直传

1️⃣ 客户端请求上传凭证 2️⃣ 服务端校验权限 + 生成 Pre-signed URL 3️⃣ 客户端直传 S3/R2 4️⃣ 客户端通知上传完成 5️⃣ 服务端确认 + 入库
// 服务端:生成预签名上传 URL
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({ region: 'auto', endpoint: '...' });

async function getUploadUrl(userId, fileName, contentType) {
  // 1. 校验用户权限
  const allowed = await canUpload(userId);
  if (!allowed) throw new Error('Forbidden');

  // 2. 生成安全 Key(防止路径遍历)
  const ext = fileName.split('.').pop();
  const key = `uploads/${userId}/${crypto.randomUUID()}.${ext}`;

  // 3. 生成预签名 URL (5 分钟有效)
  const url = await getSignedUrl(
    s3,
    new PutObjectCommand({
      Bucket: process.env.BUCKET_NAME,
      Key: key,
      ContentType: contentType,
      // 限制大小:Content-Length 范围 (S3 不直接支持,需客户端配合)
    }),
    { expiresIn: 300 }
  );

  return { url, key };
}
// 前端:使用预签名 URL 直传
async function uploadFile(file) {
  // 1. 获取预签名 URL
  const { url, key } = await fetch('/api/upload-url', {
    method: 'POST',
    body: JSON.stringify({
      fileName: file.name,
      contentType: file.type,
    }),
  }).then(r => r.json());

  // 2. 直传 S3/R2
  const res = await fetch(url, {
    method: 'PUT',
    body: file,
    headers: { 'Content-Type': file.type },
  });

  if (!res.ok) throw new Error('Upload failed');

  // 3. 通知服务端上传完成
  await fetch('/api/upload-confirm', {
    method: 'POST',
    body: JSON.stringify({ key }),
  });
}
⚠️ 直传安全要点
① Pre-signed URL 的 ContentType 必须限定(防止上传 HTML/SVG 执行 XSS)
② Key 必须服务端生成(防止路径遍历、覆盖他人文件)
③ 文件大小限制:服务端记录配额,前端 Content-Length 校验 + 服务端事后校验
④ 上传完成后服务端必须确认(否则有孤儿文件)

7. 大文件:分片上传、断点续传

S3/R2 单次 PUT 限制 5GB。超过 5GB 必须分片上传 (Multipart Upload)。实际上,超过 100MB 就应该用分片上传。

分片上传流程

CreateMultipartUpload UploadPart × N
(并行上传)
CompleteMultipartUpload
// 前端大文件分片上传(S3 Multipart)
import { Upload } from '@aws-sdk/lib-storage';

async function uploadLargeFile(file) {
  const upload = new Upload({
    client: s3Client,
    params: {
      Bucket: 'my-bucket',
      Key: `uploads/${crypto.randomUUID()}/${file.name}`,
      Body: file,
      ContentType: file.type,
    },
    // 分片配置
    partSize: 10 * 1024 * 1024, // 10MB per part
    queueSize: 4,               // 并行 4 个分片
    leavePartsOnError: true,    // 失败保留已传分片(可断点续传)
  });

  // 进度回调
  upload.on('httpUploadProgress', (progress) => {
    const pct = ((progress.loaded / progress.total) * 100).toFixed(1);
    console.log(`Upload: ${pct}%`);
  });

  await upload.done();
}

断点续传实现

// 断点续传核心逻辑
const uploadState = {
  uploadId: null,           // S3 返回的 Upload ID
  completedParts: [],       // 已完成的 Part 列表 [{PartNumber, ETag}]
  file: null,
  key: null,
};

// 恢复上传
async function resumeUpload() {
  if (!uploadState.uploadId) return;

  // 查询已上传的分片
  const { Parts } = await s3.send(new ListPartsCommand({
    Bucket: bucket,
    Key: uploadState.key,
    UploadId: uploadState.uploadId,
  }));

  // 只上传缺失的分片
  const completedNums = new Set(Parts.map(p => p.PartNumber));
  const totalParts = Math.ceil(uploadState.file.size / PART_SIZE);

  for (let i = 1; i <= totalParts; i++) {
    if (completedNums.has(i)) continue;
    // 上传该分片...
  }
}

大文件最佳实践

8. 图片处理:Resize / 格式转换 / CDN

SaaS 产品中图片是最常见的 UGC 类型。直接存原图会浪费带宽和加载时间——一个 5MB 的手机照片,缩略图只需 20KB。

图片处理方案对比

方案实现实时成本灵活性
Cloudflare Image ResizingCDN 边缘处理$0.50/10K高(参数化)
imgproxy自托管 Go 服务服务器成本极高
Sharp (Node.js)应用层处理服务器成本极高
上传时预处理Lambda/Worker 触发❌ 异步按调用量固定尺寸
SaaS: Cloudinary托管图片 CDN$89+/月极高

推荐:imgproxy 自托管

imgproxy 是最快的开源图片处理服务,用 libvips C 库,比 Sharp 快 5-10 倍。支持签名 URL 安全访问。

# imgproxy Docker 部署
docker run -d \
  --name imgproxy \
  -p 8080:8080 \
  -e IMGPROXY_KEY=$(openssl rand -hex 16) \
  -e IMGPROXY_SALT=$(openssl rand -hex 16) \
  -e IMGPROXY_ENABLE_URL_SIGNATURE=true \
  -e IMGPROXY_MAX_SRC_RESOLUTION=20 \
  -e IMGPROXY_USE_ETAG=true \
  -e IMGPROXY_ENABLE_WEBP_DETECTION=true \
  darthsim/imgproxy
// imgproxy URL 签名 + 生成缩略图 URL
import crypto from 'crypto';

function generateImageUrl(s3Url, width, height) {
  const path = `/rs:fit:${width}:${height}/g:sm/${Buffer.from(s3Url).toString('base64')}`;
  const signature = crypto
    .createHmac('sha256', IMGPROXY_KEY + IMGPROXY_SALT)
    .update(path)
    .digest('base64url');
  return `https://img.example.com/${signature}${path}`;
}

// 使用:
// https://img.example.com/{sig}/rs:fit:200:200/g:sm/{base64_s3_url}
// 自动返回 WebP 格式(浏览器支持时)

图片处理流水线

上传原图
S3/R2
S3 Event
→ Lambda/Worker
生成缩略图
200/400/800
存回 S3
thumbs/ 路径
CDN 缓存
全球加速

格式策略

🧠 最佳实践:On-the-fly + CDN 缓存
不在上传时预生成缩略图——用 imgproxy/Cloudflare Image Resizing 按需生成,
CDN 缓存结果。优点:① 节省存储 ② 任意尺寸 ③ 格式自动适配 ④ 缓存失效自动重生成

9. CDN 策略:缓存、回源、预热

对象存储解决「存」,CDN 解决「快」。全球用户访问 S3 美东区域 = 200-400ms 延迟,经过 CDN = 10-50ms。

CDN 选型

CDN流量费与存储集成特色
CloudflareFree(R2)/ $0.05-0.12/GBR2 无缝全球节点最多、Workers 边缘计算
AWS CloudFront$0.02-0.14/GBS3 无缝Lambda@Edge、S3 Transfer
bunny.net$0.01/GB需自定义源极低价、边缘存储
国内:腾讯/阿里 CDN¥0.20/GBCOS/OSS 回源免费国内节点多、ICP 备案

CDN 缓存策略

// R2 自定义域名 + Cache-Control
// 上传时设置 HTTP 头
await s3.send(new PutObjectCommand({
  Bucket: 'my-bucket',
  Key: key,
  Body: fileBuffer,
  ContentType: 'image/webp',
  CacheControl: 'public, max-age=31536000, immutable',  // 1 年,内容不可变
}));

// 用户头像等可变内容
CacheControl: 'public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800'
// 浏览器缓存 1 小时,CDN 缓存 1 天,过期后 7 天内可返回旧内容同时后台刷新

Cache-Control 参数详解

参数作用场景
max-age=31536000缓存 1 年不可变资源(hash 文件名)
immutable告诉浏览器不重新验证hash 文件名
s-maxage=86400CDN 缓存 1 天(覆盖 max-age)CDN 独立 TTL
stale-while-revalidate过期后返回旧内容 + 后台刷新允许短时间过期的场景
no-cache每次都验证(非不缓存)频繁更新的资源
private不允许 CDN 缓存签名 URL 的私有内容

缓存失效策略

🔑 不可变文件名 (推荐)

Key 包含 hash:/avatars/{hash}.webp

更新 = 新 Key,旧 Key 永不过期。缓存 1 年。

零缓存失效问题。

🧹 版本化 Key

Key 含版本号:/avatars/123/v2.jpg

更新 = 新版本路径,数据库记录当前版本。

简单有效,略增存储。

10. 安全:签名 URL、CORS、加密

预签名 URL 安全模型

预签名 URL = 把临时凭证嵌入 URL。任何拿到 URL 的人都能在有效期内操作。

// 生成下载签名 URL (只读,1 小时有效)
import { GetObjectCommand } from '@aws-sdk/client-s3';

async function getDownloadUrl(key) {
  return await getSignedUrl(
    s3,
    new GetObjectCommand({
      Bucket: 'my-bucket',
      Key: key,
      ResponseContentType: 'application/octet-stream',
      ResponseContentDisposition: 'attachment; filename="download"',
    }),
    { expiresIn: 3600 }
  );
}

签名 URL 安全要点

CORS 配置

// S3 Bucket CORS 配置 — 允许前端直传
[
  {
    "AllowedHeaders": ["Content-Type", "x-amz-*"],
    "AllowedMethods": ["PUT", "GET"],
    "AllowedOrigins": ["https://app.example.com"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
  }
]
⚠️ CORS 陷阱
AllowedOrigins: ["*"] + 签名 URL = 任何网站都能用你的签名 URL 上传/下载
② 必须限定具体域名:["https://app.example.com"]
③ S3 CORS 是 Bucket 级别,不同桶可以不同策略
④ 预检请求 (OPTIONS) 不带签名,S3 会返回 CORS 头,浏览器据此决定是否允许

加密策略

加密方式说明性能影响密钥管理
SSE-S3S3 自动加密,AWS 管密钥零配置
SSE-KMS用 AWS KMS 密钥加密极小KMS 策略控制
SSE-C客户自管密钥,每次请求传密钥极小完全自控
客户端加密上传前在客户端加密取决于实现完全自控

推荐:默认 SSE-S3(零配置零成本),合规需求用 SSE-KMS(审计轨迹),极端安全用客户端加密(密钥永远不出应用)。

11. 成本模型与优化

典型 SaaS 场景成本估算

假设:10 万用户,每人上传 20 个文件(平均 2MB),30% 日活,每天查看 50 个文件。

项目S3 + CloudFrontR2MinIO (自托管)
存储 (4TB)$92/月$60/月$0 (含在服务器)
出口 (6TB/月)$120/月$0自备带宽
请求 (5M PUT + 50M GET)$27.5/月$18/月$0
CDN$50/月Free (内置)需自建
总计$289.5/月$78/月服务器 $80-200/月
💰 R2 比S3 省 73%,且流量越大差距越大。对于读密集型 SaaS,R2 是不二之选。

成本优化清单

  1. 自动分层:30 天后 Standard → Standard-IA,90 天后 → Glacier
  2. 压缩:上传前 gzip/brotli 压缩文本文件,图片用 WebP/AVIF
  3. 去重:上传前计算文件 hash(SHA-256),已存在则引用计数
  4. CDN 缓存:immutable + max-age=1 年,减少回源
  5. 清理孤儿:定期扫描 Bucket 与数据库比对,删除未引用文件
  6. Intelligent-Tiering:S3 智能分层,自动分析访问模式迁移

12. 选型决策树

你的 SaaS 文件存储选型
│
├─ 数据必须留在自己机房?
│   ├─ 是 → MinIO (自托管, S3 兼容, 纠删码)
│   └─ 否 ↓
│
├─ 主要用户在中国大陆?
│   ├─ 是 → 腾讯 COS / 阿里 OSS + 国内 CDN
│   └─ 否 ↓
│
├─ 出口流量大 (用户频繁查看/下载文件)?
│   ├─ 是 → Cloudflare R2 (零出口费 + 内置 CDN)
│   └─ 否 ↓
│
├─ 已深度绑定 AWS 生态?
│   ├─ 是 → S3 + CloudFront (无缝集成, 功能最全)
│   └─ 否 ↓
│
├─ 预算极低 / 早期 MVP?
│   ├─ 是 → Backblaze B2 + Cloudflare CDN ($0.006/GB)
│   └─ 否 → R2 (性价比最优解)
│
💡 通用推荐:R2 是 2024-2026 最优默认选择
   零出口费 + S3 兼容 + 内置 CDN = 三杀

13. 实战代码示例

完整上传服务 (FastAPI + R2)

# main.py — FastAPI 文件上传服务
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel
from botocore.config import Config
import boto3, uuid, hashlib

app = FastAPI()

# R2 客户端 (S3 兼容)
s3 = boto3.client(
    's3',
    endpoint_url=f'https://<ACCOUNT_ID>.r2.cloudflarestorage.com',
    aws_access_key_id='...',
    aws_secret_access_key='...',
    config=Config(signature_version='s3v4'),
)

BUCKET = 'my-saas-uploads'
MAX_FILE_SIZE = 50 * 1024 * 1024  # 50MB
ALLOWED_TYPES = {'image/jpeg', 'image/png', 'image/webp', 'application/pdf'}

class UploadRequest(BaseModel):
    fileName: str
    contentType: str
    fileSize: int

@app.post('/api/upload-url')
async def get_upload_url(req: UploadRequest, user=Depends(get_current_user)):
    # 1. 校验
    if req.contentType not in ALLOWED_TYPES:
        raise HTTPException(400, f'Unsupported type: {req.contentType}')
    if req.fileSize > MAX_FILE_SIZE:
        raise HTTPException(400, f'File too large (max {MAX_FILE_SIZE//1024//1024}MB)')

    # 2. 生成安全 Key
    ext = req.fileName.rsplit('.', 1)[-1].lower()
    key = f'uploads/{user.id}/{uuid.uuid4()}.{ext}'

    # 3. 生成预签名 URL
    url = s3.generate_presigned_url(
        'put_object',
        Params={
            'Bucket': BUCKET,
            'Key': key,
            'ContentType': req.contentType,
        },
        ExpiresIn=300,  # 5 分钟
    )

    return {'url': url, 'key': key}

@app.post('/api/upload-confirm')
async def confirm_upload(key: str, user=Depends(get_current_user)):
    # 校验文件确实存在且属于该用户
    try:
        obj = s3.head_object(Bucket=BUCKET, Key=key)
    except:
        raise HTTPException(404, 'File not found in storage')

    # 校验 Key 归属(防止用户确认别人的文件)
    if not key.startswith(f'uploads/{user.id}/'):
        raise HTTPException(403, 'Not your file')

    # 入库
    file_record = await db.files.create(
        key=key,
        size=obj['ContentLength'],
        content_type=obj['ContentType'],
        user_id=user.id,
    )

    # 返回下载 URL
    download_url = s3.generate_presigned_url(
        'get_object',
        Params={'Bucket': BUCKET, 'Key': key},
        ExpiresIn=3600,
    )

    return {'id': file_record.id, 'downloadUrl': download_url}

文件去重服务

# dedup.py — 上传前文件去重
import hashlib

async def check_duplicate(file_content: bytes, user_id: str) -> dict | None:
    # 计算文件 SHA-256
    file_hash = hashlib.sha256(file_content).hexdigest()

    # 查数据库是否已有相同 hash
    existing = await db.files.find_unique(where={'hash': file_hash})

    if existing:
        # 已存在:增加引用计数,不重复存储
        await db.file_refs.create(
            data={'file_id': existing.id, 'user_id': user_id}
        )
        return {'id': existing.id, 'duplicate': True}

    return None  # 不重复,继续上传

14. 替代方案与特殊场景

数据库存储 (BLOB)

小型文件(< 1MB)如头像缩略图可以直接存数据库 BLOB。PostgreSQL 的 BYTEA 性能不错,但有上限(1GB)且不能 CDN 加速。仅适合低频访问的元数据附件

IPFS / Arweave (去中心化存储)

NFT/链上应用用 IPFS 存元数据,Arweave 做永久存储。延迟高、检索难、成本不可预测。不推荐传统 SaaS 使用

Supabase Storage

Supabase 封装了 S3 + RLS 策略 + CDN,开箱即用。底层是 S3。适合快速 MVP,但成本比直接用 R2 高 2-3 倍。

// Supabase Storage — 最简上传
const { data, error } = await supabase.storage
  .from('avatars')
  .upload(`user-${userId}/avatar.jpg`, file, {
    cacheControl: '3600',
    upsert: true,
  });

特殊场景速查

场景推荐方案原因
视频流 (HLS/DASH)S3 + CloudFront + MediaConvert转码流式分发
文档协作R2 + CRDT (Yjs) + WebSocket实时同步
数据备份S3 Glacier / B2极低成本归档
日志归档S3 + Athena / R2 + Worker 分析SQL 查询冷数据
邮件附件R2 + Worker 代理签名 URL 控制有效期
用户导出 (CSV/Excel)R2 + 异步生成 + 签名下载大文件异步生成,签名 URL 限时下载

Agent 架构研究 中,多个 Agent 项目涉及文件存储的独特设计:

🔧 Agent 工具系统中的文件操作

几乎所有 Agent 都需要文件操作能力:

🧠 Agent 记忆系统 = 特殊的文件存储

build-memory 分析揭示了一个有趣模式:Agent 的「记忆」本质上是特殊结构的文件存储:

启示:Agent 记忆系统 = 文件存储 + 向量索引 + 压缩/过期策略,本质上和 SaaS 文件存储是同一问题域。

🔒 Agent 安全 = 文件安全的最严格形态

build-safety 分析中:

启示:SaaS 文件存储安全可以借鉴 Agent 的多层防护——签名 URL 是第一层,服务端校验是第二层,沙箱隔离是第三层。


📁 文件存储 — SaaS 全栈知识库 | 返回目录 | Agent 架构研究