Files
go-omni/storage/README.md

116 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 对象存储与上传
核心 `storage.Store` 保持 `Put/Get/Delete`,不会强迫每个适配器实现云厂商的所有能力。业务可按需依赖可选接口:
| 能力 | 接口 | Local | S3 / R2 | OSS |
| --- | --- | --- | --- | --- |
| 读写删除 | Store | 支持 | 支持 | 支持 |
| 临时下载 | Presigner | NewSigner | 支持 | 支持 |
| 限定大小/类型的直传 | UploadPresigner | NewUploadSigner | 支持 | 支持,需要配置签名 Header |
| 创建、上传、查询、完成、取消分片 | MultipartStore | 当前不提供 | 支持 | 支持 |
| 分片直传签名 | PartPresigner | 当前不提供 | 支持 | 支持,需要配置签名 Header |
没有固定业务字段、目录规则或身份模型。鉴权、对象归属、业务配额由应用定义。SDK 客户端和连接池由应用持有;适配器不会在一次调用后关闭客户端。
## 服务端上传限制
```go
uploader, err := storage.NewUploader(files, storage.UploadPolicy{
MaxBytes: 32 << 20,
ContentTypes: []string{"image/png", "image/jpeg", "application/pdf"},
})
if err != nil { return err }
info, err := uploader.Put(ctx, key, reader, size, contentType)
```
`files` 是任意 Store。MaxBytes 必须为正数;ContentTypes 是精确 MIME 白名单,忽略参数,不接受通配符。空白名单允许所有合法声明类型。单次上传检查声明大小、流的实际长度;设置类型白名单时,再以头部最多 512 字节嗅探结果核对声明类型。适配器本身的大小限制仍然有效,建议与策略统一。这里只读取有限前缀,不把完整文件放进内存。
MIME 嗅探用于粗粒度识别,不验证完整文件结构或病毒;CSV/JSON 等类型可能被嗅探成 text/plain。需要复杂格式判断时,在业务校验流程中处理。必须传入可终止、正确返回 EOF 的 reader;ContextReader 不能中断任意用户 Reader 中已经阻塞的 Read。
使用 Local 时先写临时文件,校验失败不会替换旧对象。云端网络中断可能产生结果未知或遗留未完成上传;使用唯一暂存 key,并通过查询/对账确认后再更新业务记录,不要把上传 error 当成绝对未写入。
## 云端直传
```go
// files 为 S3/R2 或 OSS 的 Store;Uploader 统一执行策略检查。
uploader, err := storage.NewUploader(files, storage.UploadPolicy{
MaxBytes: 32 << 20, ContentTypes: []string{"image/png", "image/jpeg"},
})
if err != nil { return err }
// 业务先授权并生成暂存 key,随后签发。
request, err := uploader.PresignPut(ctx, key, size, "image/png", 5*time.Minute)
```
返回 `method/url/headers/expiresAt`,有效期 1 秒到 1 小时。签名绑定 key、方法、确切 Content-Length 和声明 Content-Type,缺少这些签名约束时适配器报错。SDK 凭据提前失效时,链接也可能提前失效。
浏览器请求使用原始 File/Blob 作为 body,不能额外包 FormData;Content-Length 由浏览器按 body 长度生成,禁止手动设置。其余 Headers 按返回值原样发送:
```js
const headers = new Headers();
for (const [name, values] of Object.entries(request.headers)) {
if (name.toLowerCase() !== 'content-length') headers.set(name, values.join(', '));
}
const response = await fetch(request.url, {
method: request.method, headers, body: file,
});
if (!response.ok) throw new Error(`upload failed: ${response.status}`);
```
桶的 CORS 由部署方配置,允许业务域名、PUT 和返回的请求头;分片直传还需暴露 ETag 响应头。前端不能得到永久凭据。链接在到期前可能重复使用并覆盖同一个 key,不是一次性凭证。用随机暂存 key,业务确认前不要当作已发布文件。
**云端直传的类型限制针对声明 Header,无法由 API 服务实时检查文件字节。** 内容校验/扫描需要后处理;验证通过前不要公开文件。前端报告“上传完成”不等于文件可信。具体平台仍需实网验收,不能仅凭 S3 兼容声明判断一致。
OSS 使用官方 Go SDK v2:
```go
client := ossSdk.NewClient(ossSdk.LoadDefaultConfig().
WithRegion("cn-hangzhou").
WithSignatureVersion(ossSdk.SignatureVersionV4).
WithAdditionalHeaders([]string{"content-length"}).
WithCredentialsProvider(credentialProvider))
files, err := oss.New(client, bucket, 32<<20)
```
适配器不修改传入的 client;缺少签名大小约束会返回明确错误。SDK 的 presign 实现与返回 SignedHeaders 都经过检查。
## 本地签名上传
```go
files, err := local.New("./privateFiles", 32<<20)
if err != nil { return err }
signer, err := storage.NewUploadSigner(files, "https://files.example/upload", secret,
storage.UploadPolicy{MaxBytes: 32<<20, ContentTypes: []string{"image/png"}})
if err != nil { return err }
// signer 实现 http.Handler,可挂到现有的 HTTP 服务;secret 至少 32 字节。
request, err := signer.PresignPut(ctx, key, size, "image/png", time.Minute)
```
Handler 检查签名、有效期、单值参数、Content-Length/Content-Type,以及实际内容大小和嗅探类型,成功返回 201。本地存储只开放一个受控入口,不把磁盘目录映射成公共目录。原 `NewSigner` 继续只服务下载。请求超时、CORS、并发限制和速率限制由承载该 Handler 的服务配置。
## 分片与续传
S3/R2/OSS 均实现 `MultipartStore` 和 `PartPresigner`。Uploader 也暴露相同操作;不支持的底层返回 `ErrUnsupported`。
```go
session, err := uploader.CreateMultipart(ctx, key, "application/pdf")
if err != nil { return err }
// 业务持久保存 session,与已授权调用者关联,不能信任客户端任意传入的 key/uploadId。
part, err := uploader.UploadPart(ctx, session, 1, reader, size)
// 或签发单个分片的 PUT 请求:
request, err := uploader.PresignPart(ctx, session, 1, size, 5*time.Minute)
// 重连后分页读取实际已上传的分片,NextMarker == 0 表示结束。
page, err := uploader.ListParts(ctx, session, 0, 1000)
// 按 Number 升序从 1 连续提交,ETag 使用存储平台的返回值。
info, err := uploader.CompleteMultipart(ctx, session, parts)
// 用户取消/任务过期时,用仍有效的 context 显式清理。
err = uploader.AbortMultipart(ctx, session)
```
以上为互斥分支的 API 索引,实际调用需逐步处理 error。续传由业务保存 session,查询已有分片后重传缺失部分;框架不维护有身份语义的上传任务表,也不代替业务调度重试。
统一边界:每个对象最多 10,000 片、每片最多 5 GiB,除末片外至少 5 MiB;这是适配层为跨平台复用选取的共同约束。空文件走普通 Put。ListParts 每页最多 1,000 条。平台/桶限制可能更低。默认单对象仍为 32 MiB,需要大文件时显式提高适配器与 UploadPolicy 的限制;OSS 简单 Put 和云端单次签名 PUT 最大 5 GiB,更大使用分片。
Complete 会向平台分页核对全部分片的序号、ETag、真实大小和累计上限,前端填写的 `Part.Size` 不参与信任判断。完成时停止并发替换分片;Provider 仍会再次校验 ETag。返回 Info 的 Size 为已核对大小,ContentType 留空,完整对象元数据可通过 Get 获取。Multipart 的类型策略检查创建时的声明,实际内容应在暂存对象合并后校验。未完成的分片占用存储空间:应用需要超时取消或配置桶生命周期;URL 过期不会自动清理分片。
验收:Local 实际字节与原子性、S3/OSS SDK HTTP 协议测试、R2 签名配置测试、真实隔离 MinIO 验证。OSS/R2 尚未使用云账号实测。复现 MinIO 验证:先准备镜像,再运行 `python3 scripts/verifyUploads.py`;报告见 [uploadsLocal.json](../performance/uploadsLocal.json)。