feat: complete API-driven projects with infrastructure and business acceptance
This commit is contained in:
115
storage/README.md
Normal file
115
storage/README.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# 对象存储与上传
|
||||
|
||||
核心 `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)。
|
||||
Reference in New Issue
Block a user