Skip to content
bitzorcas
中EN

Concept

Files 文件资产

源码校验的 Files 模块说明书,覆盖上传会话、对象校验、资产状态、下载授权、删除、存储适配器与生产缺口。

Last updated

Files 把对象存储中的字节提升为可授权、可追踪、可被业务引用的 FileAsset。它已经实现服务端存储键、租户聚合、上传完成校验、Owner 下载策略、双 ORM 读写和 S3 预签名访问;但当前实现没有病毒扫描、配额、可信哈希计算、上传过期拦截和已删除对象回收,不能把它描述成完整的文件安全平台。

1. 模块解决什么问题

对象存储只知道 Bucket、Key 和字节,不知道租户、业务归属、授权策略或“能否绑定订单”。Files 在两者之间建立资产边界:

  • FileAsset 保存租户、Owner、原始文件名、声明元数据、对象事实和生命周期;
  • Application 用例创建直传能力、完成上传、签发下载访问和删除资产;
  • IFileStorage 隔离 Local、S3-compatible 与连接器 Provider;
  • 调用模块只保存 FileId,并继续拥有“附件属于哪个业务对象”的规则;
  • FileAssetSummary 作为只读事件/跨模块快照,不暴露可变聚合行为。

本模块不拥有业务附件表、图片裁剪、文档预览、内容识别、版本管理或合规保留决策。

2. 当前端到端路径

FileAssetSummary只保存 FileId

已认证客户端

生成式 /api/files/* 端点

Mediator
认证 / 授权 / 事务 / Activity 审计

Files Commands / Query

FileAsset
SysFileAsset

IFileStorage

Local / S3 / Connector

调用模块
Ticket / Chat / Export

这里有两个独立事实源:数据库保存资产状态,对象存储保存字节。当前代码没有跨两者的原子事务,所以创建、完成、删除都要分析“数据库成功、对象失败”或反向组合。

3. HTTP 用例

方法与路由消息动作当前结果
POST /api/files/upload-sessionCreateUploadSessionCommandCreateUploadSession,含 FileId、StorageKey、URL
POST /api/files/{fileId}/finalizeFinalizeUploadCommandUpdateFileAssetSummary
GET /api/files/{fileId}/downloadGetFileDownloadAccessQueryView五分钟语义的预签名下载描述
DELETE /api/files/{fileId}DeleteFileCommandDelete软删除资产并发布删除摘要

四个消息都实现 IAuthorizedRequest。治理目录声明的权限为 files.file.create/view/update/delete。下载处理器在通用 View 授权后,还会执行 FileAssetOwnerPolicy;删除处理器没有 Owner Policy,因此拥有 Delete 权限的调用者在有效租户内可删除任意文件。

4. 生命周期与绑定红线

create sessionmetadata verifiedfinalizehandler verifies thenadvances twicedeletedeletedeleterepeated finalize / conflictrepeated delete / conflict

PendingUpload

Uploaded

Finalized

Deleted

CanBindToBusiness() 仅在 Finalized 返回 true。对象已经存在、数据库状态是 Uploaded、拥有 StorageKey 或拿到上传 URL,都不能替代这条领域判断。

FileAsset.Restore 是 ORM 物化工厂,不是“恢复已删除文件”的业务操作。当前没有 Restore 端点或 Deleted 到 Finalized 的状态迁移。

5. 信任边界

数据来源当前信任程度
TenantIdICurrentUser.User.TenantId服务端上下文,但未使用 EffectiveTenantId
OwnerId当前 UserId,否则 ClientId服务端派生
OwnerType / Visibility客户端只做非空与长度校验,没有注册表
FileName客户端不进入 StorageKey;仍会进入 DTO/响应头
Size / ContentType创建命令finalize 时与对象元数据比较
ContentHashfinalize 命令或对象 metadataProvider 没有 SHA-256 时会回退信任客户端
StorageKey服务端 UUID v7tenants/{tenantId}/{fileId}

文件名即使是 ../file.pdf 也不会造成当前直传 Key 穿越,因为 Key 完全由服务端生成。它仍然属于非可信展示数据,下载响应、日志和 UI 必须分别转义。

6. 持久化模型

FileAsset 直接作为两个 ORM 的统一聚合模型写入 SysFileAsset:

  • 表开启租户与软删除;
  • StorageKey 有全局唯一索引;
  • Tenant + Owner、Tenant + Status 有普通索引;
  • StatusName 写入数据库列 Status,未知值在物化时失败关闭;
  • 非 Pending 状态必须有 ContentHash,Finalized 必须有 FinalizedAt;
  • StorageKey 必须以 tenants/{tenantId}/ 或 {tenantId}/ 开头。

字段长度在聚合工厂中提前验证:FileId 36、OwnerType 50、OwnerId 64、Visibility 20、StorageKey 512、ContentHash/ETag 128、ContentType 100、FileName 255。

7. 存储 Provider 现实矩阵

Provider直接读写预签名上传元数据可信度预签名下载适用性
UnavailableFileStorage否否否否Shell 失败关闭
LocalFileStorage是否长度可信;类型/哈希为空否端口测试或自定义直传,不支持当前 HTTP 闭环
S3CompatibleFileStore是是Length/Type/ETag;SHA 取 user metadata是当前生产路径
FileStoreAdapter取决于连接器取决于连接器只统一 Length取决于连接器桥接面已实现,API 组合根当前未调用注册扩展

Production/Staging 配置门禁只接受 Minio,并要求 Endpoint、AccessKey、SecretKey。默认 appsettings.json 使用 Unavailable。

8. 当前最重要的实现缺口

  1. 没有最大文件大小、租户配额、MIME allowlist、Magic Bytes 或恶意内容扫描。
  2. 预签名 PUT 没有内容长度、Content-Type 或 checksum 条件。
  3. UploadExpiresAt 被保存,却从未在 finalize 中检查。
  4. S3 缺少 x-amz-meta-sha256 时,finalize 接受客户端 ContentHash;连接器路径还会接受客户端 ContentType。
  5. 重复 finalize 和 delete 都返回冲突,没有请求幂等键或结果重放。
  6. 创建时先签发 URL 后保存数据库;保存失败会留下仍可用的上传能力。
  7. finalize 保存和事件发布处于数据库事务路径,但对象已经在外部存储,回滚不能删除对象。
  8. Delete 只软删除数据库记录,不调用对象存储删除。
  9. 孤儿清理会立即选中所有 PendingUpload、忽略 UploadExpiresAt,也不处理 Deleted 文件。
  10. 下载 DTO 暴露 StorageKey;连接器忽略请求 TTL 和 FileName,返回的 ExpiresAt 可能与 URL 实际过期不一致。

9. 调用模块的正确责任

以 Ticket 附件为例,Ticket 模块应验证:

Ticket 绑定文件的领域边界示例
// 先通过 Files 的只读契约取得资产事实;不要直接查询 SysFileAsset。
var asset = await fileAssets.GetSummaryAsync(command.FileId, cancellationToken);
// 业务 Owner 负责“这个文件能否成为当前工单附件”。
if (asset.IsFailure || !asset.Value.Status.Equals(FileAssetStatus.Finalized))
return Result.Failure(TicketErrors.AttachmentNotReady);
if (asset.Value.TenantId != currentTenant.EffectiveTenantId)
return Result.Failure(TicketErrors.AttachmentTenantMismatch);
// 业务表只保存稳定 FileId,不复制 URL、Bucket 或 StorageKey。
ticket.Attach(command.FileId, command.DisplayName);

这是基于当前公开契约的消费示例,不是源码逐字复制。真实调用还需要把业务 OwnerType/OwnerId 与附件归属规则闭环;当前创建接口把 OwnerId 固定为调用者身份,并不能登记任意业务对象 ID。

10. 阅读路径

  1. 上传会话与客户端直传
  2. 完成上传与完整性
  3. 下载授权与 Owner Policy
  4. 存储组合、删除与清理
  5. 测试、运营与 GA 门禁

关联基础章节:Authorization、Multitenancy、Auditing 和 GDPR。

11. 最小源码审查

Terminal window
# 列出四个公开用例、路由和授权动作。
rg -n "GenerateEndpoint|AuthorizationAction" src/Platform/Files -g '*.cs'
# 查找真正的扫描、配额与过期拦截;当前预期没有生产命中。
rg -n "Virus|Malware|Quota|UploadExpiresAt.*clock|MagicBytes" \
src/Platform/Files src/Hosts -g '*.cs'
# 核对删除是否触达对象存储;当前 DeleteFile 处理器没有 IFileStorage。
rg -n "DeleteAsync|DeleteFileCommandHandler" \
src/Platform/Files src/Framework/BitzOrcas.Infrastructure.SqlSugar -g '*.cs'

返回平台模块目录

100%

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