S3 文件存储(Aegis.FileManager.S3)
Aegis.FileManager.S3 是 Aegis.FileManager 的 S3 协议兼容存储实现,支持 AWS S3、MinIO、Ceph 等所有兼容 S3 协议的对象存储服务。
组件概览
| 字段 | 说明 |
|---|---|
| 组件名称 | S3 文件存储 |
| 真实类库 | Aegis.FileManager.S3 |
| 组件定位 | 基于 S3 协议的对象存储文件管理实现 |
| 引入方式 | 安装 NuGet,在 Startup 中手动调用 AddS3Source 或 AddDefaultS3FileManager |
| 核心能力 | IFileManager 实现、IFileStorage 容器管理、Presigned URL、跨 Bucket 复制 |
| 主要配置 | S3Storage、FileValidation(可选) |
| 典型配套 | 文件管理基础层(Aegis.FileManager) |
什么时候要用它
适合场景:
- 你要把文件存到 AWS S3 或兼容 S3 协议的对象存储(MinIO、Ceph、DigitalOcean Spaces 等)
- 你需要 Presigned URL 让客户端直接下载,不经过业务服务器中转
- 你需要跨 Bucket / 跨存储组复制文件
- 你的部署环境是多节点、需要共享文件存储,且不想依赖 NAS 挂载
不适合场景:
- 单机部署、只需要本地文件读写(用 NAS 文件存储 更简单)
最小可运行路径
第一步:安装组件
dotnet add package Aegis.FileManager.S3
第二步:准备配置
在 appsettings.json 中添加 S3Storage 配置节:
{
"S3Storage": {
"Endpoint": "http://localhost:9000",
"Region": "us-east-1",
"AccessKey": "minioadmin",
"SecretKey": "minioadmin",
"DefaultBucket": "aegis-default",
"PresignedUrlExpiryMinutes": 15,
"ForcePathStyle": true,
"AutoCreateBucket": true
}
}
第三步:在 Startup 中注册
using Aegis.FileManager.S3;
using Aegis.FileManager.S3.Utils;
// 方式一:注册默认 S3 文件管理器(推荐入门用法)
services.AddDefaultS3FileManager(ConfigManager.Get<S3Options>("S3Storage"));
// 方式二:注册自定义 S3 源(支持扩展)
// services.AddS3Source<S3FileManager>(ConfigManager.Get<S3Options>("S3Storage"));
如果你同时需要 NAS 和 S3,可以分别注册不同命名的源。但注意 IFileManager 只能有一个默认注入绑定,多源场景建议直接注入具体的实现类(如 S3FileManager)。
第四步:在业务里使用
public class AttachmentService
{
private readonly S3FileManager _s3FileManager;
public AttachmentService(S3FileManager s3FileManager)
{
_s3FileManager = s3FileManager;
}
public async Task<bool> Upload(string fileName, Stream stream)
{
return await _s3FileManager.UploadFileAsync(fileName, stream, FileType.File, "attachments");
}
public async Task<string> GetDownloadUrl(string fileName)
{
return await _s3FileManager.GetFileUriAsync(fileName, "attachments");
}
}
配置项说明(S3Options)
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Endpoint | string | — | S3 服务端点 URL。AWS S3 可留空;自部署服务(MinIO 等)必填 |
Region | string | "us-east-1" | AWS Region 标识。自部署服务通常填 "us-east-1" 即可 |
AccessKey | string | — | Access Key ID |
SecretKey | string | — | Secret Access Key |
DefaultBucket | string | — | 默认 Bucket 名称。当 groupName 参数为空时使用此值 |
PresignedUrlExpiryMinutes | int | 15 | Presigned URL 过期时间(分钟) |
ForcePathStyle | bool | false | 是否使用 Path Style 寻址。MinIO 必须设为 true;AWS S3 通常设为 false |
AutoCreateBucket | bool | false | 上传文件时,如果目标 Bucket 不存在是否自动创建 |
AWS S3 配置示例
{
"S3Storage": {
"Region": "ap-southeast-1",
"AccessKey": "AKIAIOSFODNN7EXAMPLE",
"SecretKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"DefaultBucket": "my-app-files",
"PresignedUrlExpiryMinutes": 30,
"ForcePathStyle": false,
"AutoCreateBucket": false
}
}
AWS S3 不需要填写 Endpoint,SDK 会根据 Region 自动推导。
MinIO 配置示例
{
"S3Storage": {
"Endpoint": "http://192.168.1.100:9000",
"Region": "us-east-1",
"AccessKey": "minioadmin",
"SecretKey": "minioadmin",
"DefaultBucket": "aegis-default",
"PresignedUrlExpiryMinutes": 15,
"ForcePathStyle": true,
"AutoCreateBucket": true
}
}
MinIO 必须设置 ForcePathStyle: true,因为 MinIO 不支持 Virtual-Hosted Style 寻址。
Ceph RADOS 配置示例
{
"S3Storage": {
"Endpoint": "http://ceph-gateway.example.com:7480",
"Region": "default",
"AccessKey": "your-access-key",
"SecretKey": "your-secret-key",
"DefaultBucket": "my-ceph-bucket",
"ForcePathStyle": true,
"AutoCreateBucket": false
}
}
groupName 与 Bucket 的映射规则
S3 实现中,groupName 参数直接映射为 S3 Bucket 名称:
| groupName 参数 | 实际 Bucket | 说明 |
|---|---|---|
"" (空字符串) | DefaultBucket 配置值 | 使用默认 Bucket |
"attachments" | attachments | 直接使用 groupName 作为 Bucket 名 |
"my-bucket" | my-bucket | 同上 |
S3 Bucket 命名有严格约束(3-63 字符、只允许小写字母/数字/连字符、不能以连字符开头结尾等)。传入 groupName 时请确保符合 S3 Bucket 命名规则。
与 NAS 后端的区别:NAS 后端中 groupName 是 RootPath 下的子目录名;S3 后端中 groupName 直接就是 Bucket 名。
IFileManager 接口方法与 S3 API 对应关系
文件操作
| IFileManager 方法 | S3 API | 说明 |
|---|---|---|
DoesFileExistAsync | HeadObject | 通过获取元数据判断文件是否存在 |
UploadFileAsync (byte[]) | PutObject | 字节数组包装为 MemoryStream 后上传 |
UploadFileAsync (Stream) | PutObject | 直接使用流上传,自动推断 ContentType |
UploadFileAsync (filePath) | PutObject | 读取本地文件后上传 |
GetFileStreamAsync | GetObject | 返回 ResponseStream(不可 Seek) |
GetFileBytesAsync | GetObject | 读取完整 ResponseStream 后返回字节数组 |
GetFileAsync | GetObject | 下载到本地指定路径 |
GetFileInfoAsync | HeadObject | 获取 ContentLength、ETag、LastModified、ContentType、Metadata |
GetFileUriAsync | GetPreSignedURL | 生成 Presigned URL |
DeleteFileAsync | DeleteObject | 删除单个对象 |
ListFilesAsync | ListObjectsV2 | 支持前缀过滤和分页 |
CopyFileAsync | CopyObject | 支持跨 Bucket 复制 |
流的注意事项
GetFileStreamAsync 返回的 ResponseStream 是不可 Seek 的流。如果你需要对流进行 Seek 操作(例如传给验证管道),FileValidationManager 会自动将其缓冲为 MemoryStream。
IFileStorage 容器管理
S3 模块同时实现了 IFileStorage 接口,提供 Bucket 级别的管理能力:
| IFileStorage 方法 | S3 API | 说明 |
|---|---|---|
GroupExistsAsync | GetBucketLocation | 检查 Bucket 是否存在 |
CreateGroupAsync | PutBucket | 创建 Bucket(幂等,已存在时返回 true) |
DeleteGroupAsync | DeleteBucket | 删除 Bucket(必须为空) |
ListGroupsAsync | ListBuckets | 列出所有 Bucket |
GetGroupInfoAsync | GetBucketLocation | 获取 Bucket 详情(名称、Region) |
使用示例:
public class StorageManagementService
{
private readonly IFileStorage _fileStorage;
public StorageManagementService(IFileStorage fileStorage)
{
_fileStorage = fileStorage;
}
// 确保 Bucket 存在
public async Task EnsureBucketExists(string bucketName)
{
if (!await _fileStorage.GroupExistsAsync(bucketName))
{
await _fileStorage.CreateGroupAsync(bucketName);
}
}
// 列出所有 Bucket
public async Task<List<StorageGroupInfo>> ListAllBuckets()
{
return await _fileStorage.ListGroupsAsync();
}
}
Presigned URL 机制
GetFileUriAsync 返回的不是文件路径,而是一个带签名的临时访问 URL(Presigned URL)。
工作原理:
- 调用方请求
GetFileUriAsync("report.pdf", "documents") - S3 模块使用
GetPreSignedURL生成一个包含签名参数的 URL - 客户端可以直接通过该 URL 下载文件,无需经过业务服务器
- URL 在
PresignedUrlExpiryMinutes分钟后失效
典型用途:
- 前端直接下载大文件,减少业务服务器带宽压力
- 生成临时分享链接
- 移动端 App 直接从 S3 下载资源
安全注意:
- Presigned URL 本身包含签名凭证,过期前任何人持有该 URL 都能访问文件
- 敏感文件建议缩短过期时间(如 5 分钟),或改用服务端代理下载
- 不要将 Presigned URL 持久化存储(如写入数据库),它会过期失效
AutoCreateBucket 行为说明
当 AutoCreateBucket 设为 true 时,每次上传文件前会检查目标 Bucket 是否存在,不存在则自动创建。这省去了手动创建 Bucket 的步骤,适合以下场景:
- 开发/测试环境,快速启动
- 多租户应用,按需创建租户 Bucket
生产环境建议设为 false,由运维团队预先创建和管理 Bucket,避免应用账号拥有 CreateBucket 权限。
自定义扩展
继承 S3FileBase
如果默认的 S3FileManager 不满足需求(例如需要在上传时自动添加元数据、覆盖 ContentType 等),可以继承 S3FileBase:
public class CustomS3FileManager : S3FileBase
{
public CustomS3FileManager(IAmazonS3 client, S3Options options)
: base(client, options) { }
public override async Task<bool> UploadFileAsync(
string fileName, Stream fileStream, FileType type,
string groupName = "", CancellationToken cancellationToken = default)
{
// 添加自定义逻辑,如自动打标签、记录审计日志等
return await base.UploadFileAsync(fileName, fileStream, type, groupName, cancellationToken);
}
}
// 注册自定义实现
services.AddS3Source<CustomS3FileManager>(s3Options);
DI 注册方式对比
| 注册方法 | 注入方式 | 适用场景 |
|---|---|---|
AddDefaultS3FileManager | IFileManager + S3FileManager 均可注入 | 只有一个文件存储后端 |
AddS3Source<T> | 只能通过具体类型注入(如 S3FileManager) | 多文件源、需要区分 NAS 和 S3 |
当项目同时使用 NAS 和 S3 时,推荐用 AddS3Source<S3FileManager>(options) 注册 S3,用 AddNasSource<NasFileManager>(options) 注册 NAS,然后在业务代码中按需注入具体的实现类。
与 NAS 后端的差异对比
| 特性 | NAS 后端 | S3 后端 |
|---|---|---|
| 存储类型 | 本地文件系统 / NAS 挂载 | 对象存储(S3 协议) |
groupName 含义 | RootPath 下的子目录名 | Bucket 名称 |
GetFileUriAsync 返回值 | 本地文件绝对路径 | Presigned URL(带过期时间) |
FileObject.ContentMd5 | 计算的 MD5 哈希 | S3 ETag(分片上传时可能不是 MD5) |
GetFileStreamAsync | 可 Seek 的 FileStream | 不可 Seek 的 ResponseStream |
| 文件列举 | 基于目录遍历 | 基于 ListObjectsV2,支持分页和前缀过滤 |
容器管理(IFileStorage) | 目录的创建/删除 | Bucket 的创建/删除 |
| 大文件支持 | 受磁盘空间限制 | 理论上无限制(S3 单对象最大 5TB) |
| 多节点共享 | 需要共享挂载 | 天然支持 |
| 额外依赖 | 无 | AWSSDK.S3 |
| 文件校验 | 内置(通过 FileValidationManager) | 内置(自动处理非 Seekable 流) |
| Bucket 自动创建 | 不适用 | AutoCreateBucket 控制 |
| 跨组复制 | 手动实现 | CopyFileAsync 原生支持跨 Bucket |
安全注意事项
凭证管理
- 不要把
AccessKey和SecretKey硬编码在代码中或提交到版本控制 - 推荐使用环境变量或密钥管理服务注入凭证:
var options = new S3Options
{
Endpoint = Configuration["S3Storage:Endpoint"],
AccessKey = Environment.GetEnvironmentVariable("S3_ACCESS_KEY"),
SecretKey = Environment.GetEnvironmentVariable("S3_SECRET_KEY"),
// ...
}; - AWS 环境下推荐使用 IAM Role,避免静态凭证
IAM 策略建议
为应用创建专用的 IAM 用户/角色,遵循最小权限原则:
最小文件操作权限:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:HeadObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-app-*",
"arn:aws:s3:::my-app-*/*"
]
}
]
}
如果启用 AutoCreateBucket,还需要添加:
{
"Effect": "Allow",
"Action": [
"s3:CreateBucket",
"s3:GetBucketLocation"
],
"Resource": "*"
}
网络安全
- 自部署的 MinIO / Ceph 建议启用 TLS(HTTPS)
- 生产环境不要将 S3 端点暴露到公网,使用 VPC / 内网访问
- Presigned URL 生成后无法撤销,请合理设置过期时间
接入后怎么确认生效
通常用下面几项验收:
S3FileManager可以正常注入- 调用
UploadFileAsync后,在 S3 控制台或 MinIO 管理界面能看到对应文件 GetFileStreamAsync/GetFileBytesAsync能正常读取文件内容GetFileUriAsync返回的 Presigned URL 可以在浏览器中直接下载DeleteFileAsync后文件确实被删除ListFilesAsync返回的文件列表与实际一致
常见问题
连接 MinIO 时报 InvalidBucketName 或 NoSuchBucket
优先检查:
ForcePathStyle是否设为true(MinIO 必须开启)Endpoint是否包含协议前缀(应为http://host:port,不能省略http://)DefaultBucket是否已创建,或AutoCreateBucket是否为true
注入 IFileManager 时提示找不到注册
检查 Startup 中是否调用了 AddDefaultS3FileManager(而非 AddS3Source)。AddS3Source 只注册具体类型,不绑定 IFileManager。
GetFileStreamAsync 返回的流不能 Seek
这是预期行为。S3 的 ResponseStream 不可 Seek。如果需要 Seek,可以自己缓冲:
var stream = await _s3FileManager.GetFileStreamAsync(fileName);
var ms = new MemoryStream();
await stream.CopyToAsync(ms);
ms.Position = 0;
// 使用 ms...
Presigned URL 访问返回 AccessDenied
- 检查 AccessKey / SecretKey 是否正确
- 检查 Bucket 的访问策略是否允许该操作
- MinIO 环境下检查 Bucket Policy 是否已设置
与 NAS 同时注册时 IFileManager 注入冲突
IFileManager 只能绑定一个默认实现。多源场景下,请直接注入具体的实现类:
// 不要注入 IFileManager,改为注入具体类型
public class MyService
{
private readonly S3FileManager _s3;
private readonly NasFileManager _nas;
public MyService(S3FileManager s3, NasFileManager nas)
{
_s3 = s3;
_nas = nas;
}
}