Skip to content
bitzorcas
中EN

Guide

Files 存储组合、删除与清理

说明 IFileStorage、Local/S3/Connector 组合、配置、健康检查、删除语义、孤儿清理和数据生命周期风险。

Last updated

Files 的数据生命周期横跨 SysFileAsset 与对象存储。配置一个能解析的 IFileStorage 只是起点;生产交付还必须证明 API 与 JobHost 使用同一 Provider、Bucket 与 Key 规则,并且创建失败、未完成上传、业务删除和保留期结束都能最终收敛。

1. IFileStorage 端口

平台端口只有七项能力:

IFileStorage 能力面
public interface IFileStorage
{
// 字节读写与删除是基础能力,调用者仍要处理不存在和重复删除语义。
Task UploadAsync(string objectKey, Stream content, string contentType, CancellationToken ct);
Task<Stream> OpenReadAsync(string objectKey, CancellationToken ct);
Task DeleteAsync(string objectKey, CancellationToken ct);
// 预签名与元数据是 HTTP 直传闭环所需的可选 Provider 能力。
Task<string> GeneratePresignedUploadUrlAsync(string objectKey, TimeSpan ttl, CancellationToken ct);
Task<bool> ExistsAsync(string objectKey, CancellationToken ct);
Task<FileObjectMetadata> GetMetadataAsync(string objectKey, CancellationToken ct);
Task<string> GeneratePresignedDownloadUrlAsync(
string objectKey, string fileName, string contentType, TimeSpan ttl, CancellationToken ct);
}

Application 只依赖这个窄端口,不引用 AWS、MinIO 或连接器类型。Provider 不能完整实现某项能力时应抛 NotSupportedException,让用例失败关闭。

2. Provider 能力与限制

行为UnavailableLocalS3-compatibleConnector bridge
Upload/Open/Delete抛不支持支持支持委托 IFileStore
路径防护不适用GetFullPath 边界Bucket/Key取决于 Provider
Presigned PUT否否是取决于 Provider
Metadata否Length/LastModifiedHEAD factsExists + stream Length
Presigned GET否否是取决于 Provider
请求级 TTL否否支持忽略
下载文件名否否Header override忽略

FileStoreAdapter.OpenReadAsync 在连接器返回 null 时改成 Stream.Null。这会把“不存在”伪装成零字节流,消费方若依赖 OpenRead 必须额外执行 Exists 或修改合同。

3. API 生产配置

当前 API 的生产门禁只接受 FileStorage:DefaultProvider=Minio:

S3-compatible 配置骨架
{
"FileStorage": {
"DefaultProvider": "Minio",
"S3": {
"Endpoint": "https://minio.internal.example",
"AccessKey": "${FILE_STORAGE_ACCESS_KEY}",
"SecretKey": "${FILE_STORAGE_SECRET_KEY}",
"BucketName": "bitzorcas-files",
"Region": "cn-east-1",
"UseSsl": true,
"ForcePathStyle": true,
"CreateBucketIfNotExists": true
}
}
}

${...} 只表达由部署系统注入 Secret 的意图,不代表 .NET 配置自动展开该字符串。生产中用环境变量、User Secrets、Vault 或 AgileConfig Secret Reference 覆盖,禁止提交真实密钥。

容器级 FileStorage:Containers、Upload/Download TTL 和 MaxSinglePutBytes 已有 Options 字段,但当前四个 Files 用例没有读取这些配置;不能写成已生效策略。

4. API 组合顺序

Minio / S3Local / Unavailable

AddBitzOrcasGeneratedServices
TryAdd Unavailable

AddBitzOrcasCoreRuntime

AddBitzOrcasPersistenceAdapters

DefaultProvider

AddBitzOrcasS3CompatibleStorage
Add IFileStorage

AddBitzOrcasFilePlatform
TryAdd Local

S3 用普通 AddSingleton 注册,能够覆盖先前默认项。Local 使用 TryAddSingleton,而生成式 Unavailable 已经先注册;此外 API 没有为 Local 绑定 LocalFileStorageOptions。所以注释所说的“DefaultProvider=Local 自动启用开发存储”没有形成可证明的 API 运行时行为。

生产门禁拒绝 Local,降低了生产误用风险;开发环境仍应通过集成测试固定真实解析类型。

5. JobHost 配置必须独立验证

JobHost 的 AddJobHostFileStorage 路径更明确:

  • Minio/S3 注册 S3-compatible;
  • Local 调用 AddBitzOrcasStorage 并设置 BaseDirectory;
  • 其他值注册 Unavailable。

API 和 JobHost 是两个进程。它们必须使用相同 Bucket、Endpoint、凭据权限和 Key 解释,否则清理作业可能看不到 API 写入的对象。

Terminal window
# Kubernetes 中分别查看两个 Deployment 的有效非敏感配置。
kubectl -n bitzorcas get deploy bitzorcas-api bitzorcas-jobhost \
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.template.spec.containers[0].env}{"\n"}{end}'
# 凭据只检查 Secret 引用名,不输出 Secret 值。
kubectl -n bitzorcas get deploy bitzorcas-api bitzorcas-jobhost -o yaml \
| rg "secretKeyRef|FileStorage__DefaultProvider|FileStorage__S3__BucketName"

6. S3 初始化与健康检查

AddBitzOrcasS3CompatibleStorage 注册 Client、Options、Store 和 BucketManager;API 还添加 s3-storage readiness check。检查项应证明:

  1. Endpoint 可达且 TLS 信任正确;
  2. 凭据能访问目标 Bucket;
  3. Bucket 存在或初始化权限符合策略;
  4. API 具有 PUT/HEAD/GET/DELETE 所需权限;
  5. JobHost 至少具有 HEAD/DELETE;
  6. 时钟偏差不会让签名立即失效。

健康检查绿色不等于 finalize 能取得 SHA-256,也不等于 Bucket Lifecycle 与清理作业正确。

7. 删除的当前语义

Object StoragePublisherRepositoryFileAssetDelete HandlerClientObject StoragePublisherRepositoryFileAssetDelete HandlerClientno storage DeleteAsync callDELETE /api/files/{id}FindAsyncDelete()Status=Deleted, IsDeleted=trueDeleteAsync (soft delete)Publish FileAssetSummary

重复 Delete 通常无法通过普通软删除查询再次取得聚合;即便聚合层 Delete() 返回 File.AlreadyDeleted,HTTP 实际错误可能先表现为 NotFound。需要用真实仓储测试固定外部合同。

8. 孤儿清理的真实 SQL

JobHost 的 SqlSugarOrphanedFileCleanupAdapter 使用固定 24 小时阈值,但 SQL 实际是:

当前孤儿选择条件
-- Pending 分支没有任何时间条件,因此作业启用后会立即选中全部待上传记录。
-- Uploaded 分支才使用 24 小时 cutoff;Deleted 与 UploadExpiresAt 均未参与。
SELECT [Id] AS FileId, [StorageKey], [Status] AS StatusName
FROM [SysFileAsset]
WHERE ([Status] = 'PendingUpload')
OR ([Status] = 'Uploaded'
AND [FinalizedAt] IS NULL
AND [CreateTime] < @cutoff);

这意味着:

  • 所有 PendingUpload 都会立即入选,没有 CreateTime 或 UploadExpiresAt 条件;
  • Uploaded 只有超过 24 小时才入选;
  • Deleted 永远不入选;
  • UploadExpiresAt 完全没有使用;
  • 查询与元数据物理删除没有传递 CancellationToken;
  • 对象删除成功后才物理删除数据库行;对象失败则保留行重试。

作业默认关闭,默认 Cron 是每天 05:00。启用现状作业可能删除仍处于 30 分钟上传窗口中的对象,必须先修复选择条件。

9. 安全的目标清理模型

建议把不同原因拆成明确队列与状态:

原因选择条件保留/宽限删除顺序
会话过期Pending + UploadExpiresAt < now短宽限防时钟偏差对象 → 元数据
上传未完成Uploaded + 未 Finalized + age业务可配置对象 → 元数据
业务删除Deleted + DeleteTime + retention法务/回收站策略对象 → Tombstone/元数据
无元数据对象存储 Inventory - DB Key更长安全窗口标记 → 复核 → 删除
孤立元数据DB Key - 存储 Inventory立即阻止下载证据 → 元数据修复

文件删除通常需要 Outbox/Saga:数据库先记录 DeleteRequested,后台幂等删除对象,再记录 Purged。直接让 HTTP 事务跨数据库与 S3 无法获得原子性。

10. 清理配置

修复选择条件后才能启用的作业配置
{
"DataLifecycle": {
"OrphanedFileCleanup": {
"Enabled": true,
"CronExpression": "0 0 5 * * ?"
}
}
}

当前阈值 24 小时硬编码在 OrphanedFileCleanupJobExecutor,Options 中没有阈值字段。修改 Cron 不会改变孤儿判定年龄。

11. Connector 桥接边界

AddBitzOrcasFileStorageConnectors 只识别 FileStorage:ConnectorProvider=Minio,但 API/JobHost 组合根当前没有调用这个扩展。文档不能据此承诺十个连接器 Provider 开箱可用。

即使显式接线,也要解决:

  • 请求级 TTL 不可传;
  • Metadata 没有 ContentType、ETag、SHA 或 LastModified;
  • 下载 FileName 不可传;
  • null stream 变成 Stream.Null;
  • Provider 配置命名与 DefaultProvider 双入口容易漂移。

每个 Provider 都必须通过相同契约测试,不能仅验证 DI 可解析。

12. 迁移与回滚

从 Local/S3-A 迁移到 S3-B 时,不要只切配置:

  1. 冻结或双写新的上传;
  2. 按 Tenant/Key Inventory 复制对象;
  3. 比较字节数与服务端计算哈希;
  4. 更新 Provider/Bucket 元数据或保留路由表;
  5. 抽样验证下载文件名和 ContentType;
  6. 切换读流量并监控 NotFound;
  7. 保留旧源到回滚窗口结束;
  8. 最后按批准的销毁计划清理旧对象。

当前 Handler 只使用全局 IFileStorage,没有按 FileAsset.StorageProvider/BucketName 路由历史资产的 Resolver。直接切全局 Provider 会让旧资产指向新 Bucket 中不存在的 Key。

13. 本地验证

Terminal window
# Local 端口测试只证明直接读写与路径穿越防护。
dotnet test tests/BitzOrcas.Integration.Tests \
--filter FullyQualifiedName~LocalFileStorageTests
# 查看 API 与 JobHost 的不同 Provider 注册路径。
rg -n "RegisterFileStorageProvider|AddJobHostFileStorage|AddBitzOrcasFileStorageConnectors" \
src/Hosts src/Platform/Files -g '*.cs'
# 暴露孤儿 SQL 与业务删除之间的空白;Deleted 应只命中聚合/处理器,不命中清理 SQL。
rg -n "PendingUpload|Uploaded|Deleted|UploadExpiresAt" \
src/Framework/BitzOrcas.Infrastructure.SqlSugar/DataLifecycle \
src/Platform/Files -g '*.cs'

上一页:下载授权 · 下一页:测试、运营与 GA 门禁

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%