BugFree.FileStorage
1.1.2026.303-beta1510
dotnet add package BugFree.FileStorage --version 1.1.2026.303-beta1510
NuGet\Install-Package BugFree.FileStorage -Version 1.1.2026.303-beta1510
<PackageReference Include="BugFree.FileStorage" Version="1.1.2026.303-beta1510" />
<PackageVersion Include="BugFree.FileStorage" Version="1.1.2026.303-beta1510" />
<PackageReference Include="BugFree.FileStorage" />
paket add BugFree.FileStorage --version 1.1.2026.303-beta1510
#r "nuget: BugFree.FileStorage, 1.1.2026.303-beta1510"
#:package BugFree.FileStorage@1.1.2026.303-beta1510
#addin nuget:?package=BugFree.FileStorage&version=1.1.2026.303-beta1510&prerelease
#tool nuget:?package=BugFree.FileStorage&version=1.1.2026.303-beta1510&prerelease
BugFree.FileStorage
BugFree 文件存储抽象与 Provider 实现。
该模块用于将“对象存储(bucket + key)”统一映射到不同后端(本地文件系统 / OSS / COS 等),并提供:
- 单文件上传/下载/删除/存在性
- 预签名(用于参数防篡改与时效控制)
- 四段式分片上传(Initiate/UploadPart/Complete/Abort)
- Local 的磁盘选择与索引加速(可自愈)
功能特性
已实现 Provider
- Local(本地文件系统):支持单文件对象操作与分片上传(本地临时分片目录合并)。
- AliyunOss(阿里云 OSS):支持对象操作与分片上传(对接 OSS SDK)。
- TencentCos(腾讯云 COS):支持对象操作与分片上传(对接 COS SDK)。
- Minio:已提供配置模型与 ProviderType,但 Provider 目前为占位实现(尚未实现)。
安装方法
1) NuGet 引用
根据你的工程体系(应用层/组件层)选择引用:
BugFree.FileStorage
说明:仓库是多项目解决方案,若你仅编译单个项目,可能需要在构建时追加
GeneratePackageOnBuild=false以绕过打包阶段对 README / 图标等文件的检查。
2) 依赖注入
- 服务注册:
services.AddFileStorageService();
使用示例
Minimal Example(伪代码,展示调用方式)
说明:以下示例只表达“接口使用意图”。具体类型名/命名空间以仓库实际代码为准。
- 获取服务(DI):
IFileStorageService/FileStorageService
- 上传:
UploadAsync(bucket, key, stream, contentType, cancellationToken) - 下载:
GetAsync(bucket, key, cancellationToken) - 删除:
DeleteAsync(bucket, key, cancellationToken) - 生成预签名:
GeneratePresignedUrlAsync(bucket, key, expireSeconds, cancellationToken)
模块状态 / 能力矩阵
| Provider | 已实现 | 已测试(TestProject) | 单文件 Upload/Get/Delete/Exists | 分片上传 | 预签名 | 备注 |
|---|---|---|---|---|---|---|
| Local | ✔ | ✔ | ✔ | ✔ | ✔ | 预签名仅返回 Query,不直接返回 Url |
| AliyunOss | ✔ | ✔ | ✔ | ✔ | ✔ | 通过 UserSecrets 读取密钥,未配置自动跳过 |
| TencentCos | ✔ | ❌ | ✔ | ✔ | ✔ | 测试用例目前为空壳,需要补充集成测试 |
| Minio | ❌ | ❌ | ❌ | ❌ | ❌ | 仅有配置模型,占位实现 |
| 能力 | 已实现 | 已测试 | 说明 |
|---|---|---|---|
| Object:UploadAsync | ✔ | ✔ | 单文件上传(流) |
| Object:GetAsync | ✔ | ✔ | 下载(流) |
| Object:DeleteAsync | ✔ | ✔ | 删除 |
| Object:ExistsAsync | ✔ | ✔ | 存在性 |
| Presigned:GeneratePresignedUrlAsync | ✔ | ✔ | Local 返回 Query;云端返回 Url 或 Query |
| Multipart:InitiateMultipartUploadAsync | ✔ | ✔ | 分片上传初始化 |
| Multipart:UploadPartAsync | ✔ | ✔ | 上传分片 |
| Multipart:CompleteMultipartUploadAsync | ✔ | ✔ | 合并完成 |
| Multipart:AbortMultipartUploadAsync | ✔ | ✔ | 取消/清理 |
配置
配置模型为 FileStorageSetting(Config<FileStorageSetting>),支持加密存储。
示例(字段名与当前实现保持一致):
{
"ProviderType": "Local | AliyunOss | TencentCos | Minio",
"SingleFileMaxSize": 209715200,
"MultipartMinSize": 67108864,
"MultipartConcurrency": 4,
"MultipartMaxCount": 200,
"Local": {
"StoragePath": [
"D:/FileStorage",
"E:/FileStorage"
],
"SelectPolicy": "FreeSpaceFirst | Sequential | Random",
"AccessUrl": "http://localhost/FileStorage/",
"ParamEncryptKey": "<RANDOM_KEY>",
"SignedUrlExpireSeconds": 3600,
"DiskInfoCacheTtl": "00:00:20",
"MultipartExpire": "02:00:00",
"MultipartTempDirectoryMaxCount": 10000,
"FileIndexType": "String | File"
},
"AliyunOss": {
"Region": "cn-hangzhou",
"Endpoint": "https://oss-cn-hangzhou.aliyuncs.com",
"BucketName": "your-bucket",
"AccessKeyId": "<YOUR_KEY>",
"AccessKeySecret": "<YOUR_SECRET>",
"AccessUrl": "https://your-cdn.example.com/",
"SignedUrlExpireSeconds": 3600
},
"TencentCos": {
"AppId": "<APPID>",
"Region": "ap-guangzhou",
"SecretId": "<SECRET_ID>",
"SecretKey": "<SECRET_KEY>",
"BucketName": "your-bucket-123456",
"AccessUrl": "https://your-bucket-123456.cos.ap-guangzhou.myqcloud.com",
"IsHttps": true,
"ConnectionTimeout": 45000,
"ReadWriteTimeout": 45000,
"SignedUrlExpireSeconds": 3600
},
"Minio": {
"Endpoint": "http://127.0.0.1:9000",
"AccessKey": "<ACCESS_KEY>",
"SecretKey": "<SECRET_KEY>",
"BucketName": "your-bucket",
"AccessUrl": "https://your-minio.example.com/",
"UseSSL": false,
"SignedUrlExpireSeconds": 3600
}
}
Local 相关配置说明
FileIndexType:索引类型String:使用StringIndex(bucket 携带diskId|bucketName)File:使用FileIndex(落盘到index_{diskId}.idx)
Local Provider 索引(bucket + key → disk)
本地存储为提升定位性能,提供两种索引实现(见 BugFree.FileStorage.Provider.Local.Index):
FileIndex(落盘索引)
- 在内存维护
bucket + '\0' + key的集合,并按磁盘分别落盘为index_{diskId}.idx - 优点:定位快、可跨进程/重启恢复索引
- 缺点:需要刷盘(有 IO)
StringIndex(字符串索引,不落盘)
- 不维护映射表,而是将
diskId编码进 bucket 中,通过解析 bucket 直接定位磁盘 - bucket 约定格式:
"{diskId}|{bucketName}" - 优点:零维护、无刷盘
- 缺点:bucket 会暴露 diskId(不建议直接对外部用户开放)
索引属于“加速结构”,允许与真实文件状态短暂不一致:
- 索引命中但文件不存在:Local 会清理索引(自愈)。
- 索引未命中但扫描找到:Local 会写回索引(自愈)。
预签名(GeneratePresignedUrlAsync)
1) 本地 Provider 不返回 Url
Local 仅返回签名数据(例如 Query 中的 expireAt 与 sign),不直接返回可访问 Url。调用方需要自行拼装预览/下载入口,例如:
- 访问入口(示例):
/filestorage/preview?bucket={bucket}&key={key}&expireAt={expireAt}&sign={sign}
说明:访问入口的路由/控制器由业务方自行实现,FileStorage 模块只负责生成签名参数。
2) Local 固定签名原文格式
Local 签名原文固定为:
raw = "{bucket}:{key}:{expireAtUnixSeconds}"
签名算法:当前实现使用 HMACMD5,对应的密钥优先取 Local.ParamEncryptKey。
注意:
ParamEncryptKey属于机密信息,禁止下发/禁止日志输出。- 若未配置
ParamEncryptKey(例如测试环境临时构造配置),实现会退化为使用key派生密钥以保证兼容,但安全性更弱。
3) 如何验证(建议)
在你自定义的预览/下载入口中,建议按以下步骤验证:
- 解析 Query:取
expireAt与sign。 - 校验过期:当前时间(UTC)不得超过
expireAt。 - 重算签名:按同样的
raw规则 + 同样的密钥(优先Local.ParamEncryptKey)计算签名,对比sign。 - 通过
bucket/key调用GetAsync读取流并返回内容。
安全提醒:预签名只解决“防篡改 + 时效性”,不等价于业务鉴权;若对象属于私有资源,入口仍应结合登录态/权限/租户隔离等校验。
测试覆盖情况
说明:云端 Provider 通常依赖真实账号/网络/存储桶,因此默认不在仓库中提供可直接运行的集成测试(避免泄露密钥)。
已测试(TestProject)
- Local:TestProject/FileStorage/LocalFileStorageTest.cs
- 单文件上传/存在性/URL/删除
- 分片上传:初始化/上传分片/完成合并/取消
- 多路径策略:Sequential/Random/FreeSpaceFirst
- AliyunOss:TestProject/FileStorage/AliyunOssFileStorageTest.cs
- 通过 UserSecrets 读取密钥;未配置时自动跳过
- 单文件:上传/存在/预签名/删除
- 分片:初始化/上传分片/完成/取消
- Local:TestProject/FileStorage/LocalFileStorageTest.cs
未测试(仅有空壳用例,需要补充集成测试)
- TencentCos:TestProject/FileStorage/TencentCosFileStorageTest.cs
- Minio:TestProject/FileStorage/MinioFileStorageTest.cs
如何运行测试
- 运行全部测试:执行 TestProject 工程测试即可。
- 仅运行本地存储:筛选 TestFixture/用例名称包含 LocalFileStorageTest。
注意事项
- 不要提交真实密钥/密码/令牌;示例中请使用占位符。
- 本地存储(Local)当前实现不会在
GeneratePresignedUrlAsync中返回可直接访问的 Url(PresignedUrlResult.Url为空),仅返回签名参数;访问入口需由业务方自行实现并在入口里校验expireAt/sign。 - 单文件上传会受
SingleFileMaxSize限制;超过阈值请使用分片上传相关接口。
Local 路径安全与 DiskId
LocalDiskSelectorBase.CombineAndEnsureSafe(...)会对 bucket/key 做路径片段规范化与安全校验,阻止路径穿越(../盘符等)。diskId为磁盘根路径的稳定标识(基于根路径计算),用于:- 分片元数据
context.Metadata[DiskId]定位落盘磁盘 StringIndexbucket 前缀定位FileIndex的索引文件命名index_{diskId}.idx
- 分片元数据
Roadmap
| 事项 | 状态 | 说明 |
|---|---|---|
| Minio Provider 实现 | ❌ | 当前为占位实现,需要补对象操作与分片上传 |
| TencentCos 集成测试 | ❌ | 目前用例为空壳,建议加可选的 UserSecrets 驱动测试 |
| Local 预签名统一入口示例(Web API / Razor Pages) | ❌ | 仓库只提供签名参数生成,访问入口由业务实现 |
贡献指南
- Issue:请提供复现步骤、期望结果、实际结果、运行环境(OS/.NET/Provider)以及必要日志(注意脱敏)。
- PR:尽量保持改动面小;新增 Provider/能力时同步更新本 README 的能力矩阵与 Roadmap。
许可证(License)
以仓库根目录的 License 文件为准。
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- AlibabaCloud.OSS.V2 (>= 0.1.2)
- BugFree.Configuration (>= 1.1.2026.121-beta1102)
- Microsoft.Extensions.Options (>= 10.0.2)
- Tencent.QCloud.Cos.Sdk (>= 5.4.51)
-
net8.0
- AlibabaCloud.OSS.V2 (>= 0.1.2)
- BugFree.Configuration (>= 1.1.2026.121-beta1102)
- Microsoft.Extensions.Options (>= 10.0.2)
- Tencent.QCloud.Cos.Sdk (>= 5.4.51)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.1.2026.303-beta1510 | 38 | 3/3/2026 |
| 1.1.2026.127-beta1557 | 45 | 3/3/2026 |
| 1.1.2026.115-beta1541 | 64 | 1/15/2026 |
| 1.0.2026.107-beta1426 | 59 | 1/7/2026 |
| 1.0.2026.106-beta1144 | 57 | 1/6/2026 |
| 1.0.2025.1224-beta1658 | 140 | 12/24/2025 |
| 1.0.2025.1224-beta1527 | 134 | 12/24/2025 |
| 1.0.2025.1224-beta1412 | 137 | 12/24/2025 |