外观
文件上传链路模式篇
目标:让集成方在 multipart 单传、分片三段式与预签名直传之间正确选型、实现断点续传、用签名直读 / 预览 URL 取回内容、理解限额、看懂上传与 Document / Version / Blob 的关系(去重不影响版本独立),并避开链路坑。
不在本篇范围:版本列表 / 回退 / 保留策略(文档与版本模式篇);元数据字段值编码(见字段值编码契约);容器模型与层级(容器模式篇);租户存储用量 / 计费(多租户用量模式篇);错误码完整码表(见错误码参考)。
本篇是领域模型 §Version / Blob的纵深展开。multipart 最小上传见入门篇 §第一次上传。
两条链路与选型
| 链路 | 端点 | 适用 |
|---|---|---|
| multipart 单传 | POST /v1/versions/upload | 文件 < 500MB、网络稳定,一次请求传完 |
| 分片三段式 | initChunkedUpload → uploadChunk → completeChunkedUpload | 大文件、网络不稳,需断点续传 / 并发分片 |
| 预签名直传 | presign-upload → 客户端 PUT 至对象存储 → finalize-upload | 字节不经应用进程中转、直连对象存储,适合大附件 / 高并发;后端不支持预签名时自动回退到 multipart |
选型阈值:单文件较小(默认 < 500MB)且网络稳定走 multipart;文件大、或网络不稳、或需要断点续传 / 并发上传时走分片(单分片硬上限 50MB,分片大小须 ≤ 50MB)。希望上传字节不经应用进程中转、直连对象存储以承载大附件与高并发时走预签名直传——当后端不支持预签名时,presign-upload 会返回 mode=fallback 指引你退回 multipart,无需分支代码。
一、multipart 单传
两个 part:file(application/octet-stream)+ metadata(必须显式标 ;type=application/json)。
curl 示例(新建文档)
bash
curl -X POST 'https://api.atkonbase.example.com/v1/versions/upload' \
-H 'Authorization: Bearer ${accessToken}' \
-F 'file=@/local/path/report.pdf;type=application/octet-stream' \
-F 'metadata={"containerId":"${containerId}","title":"report.pdf","idempotencyKey":"${idempotencyKey}"};type=application/json'curl 示例(已有文档传新版本)
bash
curl -X POST 'https://api.atkonbase.example.com/v1/versions/upload' \
-H 'Authorization: Bearer ${accessToken}' \
-F 'file=@/local/path/report-v2.pdf;type=application/octet-stream' \
-F 'metadata={"docId":"${docId}","comment":"修订","label":"v2"};type=application/json'- 新建文档 vs 新版本:metadata 带
containerId+title→ 新建文档;带docId→ 在该文档上追加新版本。二选一。 idempotencyKey(可选)—— 去重键:命中COMPLETED且 targetContext(目标文档 / 容器上下文)完全匹配时返回已有版本而非重复上传;上下文不匹配则视为冲突。- ⚠️
metadatapart 的;type=application/json不可省略——省略后会被当成普通字符串 part、解析失败。
期望响应(关键字段)
json
{
"code": 0,
"data": {
"docId": "D1000001",
"versionId": "V1000001",
"blobId": "B1000001",
"versionNo": 1,
"contentHash": "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447",
"sizeBytes": 12,
"mimeType": "application/pdf",
"originalFilename": "report.pdf"
}
}二、分片三段式
第 1 段:初始化(POST /v1/versions/initChunkedUpload)
JSON body,声明整文件大小、分片规格、幂等键:
bash
curl -X POST 'https://api.atkonbase.example.com/v1/versions/initChunkedUpload' \
-H 'Authorization: Bearer ${accessToken}' \
-H 'Content-Type: application/json' \
-d '{
"containerId": "${containerId}",
"title": "big-archive.zip",
"totalSize": 734003200,
"chunkSize": 8388608,
"totalChunks": 88,
"idempotencyKey": "${idempotencyKey}",
"expectedContentHash": "${sha256OfWholeFile}"
}'- 必填:
totalSize/chunkSize/totalChunks/idempotencyKey;可选expectedContentHash(提供则在合并后比对整文件 SHA-256)。新建文档传containerId+title,新版本传docId。
响应(InitChunkedUploadResultDTO):
json
{
"code": 0,
"data": {
"uploadId": "U1000001",
"uploadedChunkIndexes": [0, 1, 2],
"alreadyCompletedVersionId": null
}
}uploadId—— 会话 ID,后续两段都用它。uploadedChunkIndexes—— 断点续传列表:该会话已收到的分片序号;重连时跳过这些分片,只补传缺的。alreadyCompletedVersionId—— 幂等命中:若同idempotencyKey已COMPLETED且 targetContext 完整匹配,直接返回已完成的versionId,无需再传。
第 2 段:上传分片(POST /v1/versions/uploadChunk)
multipart:file(分片字节)+ metadata(JSON,标 ;type=application/json)。分片可乱序、可重传、可并发:
bash
curl -X POST 'https://api.atkonbase.example.com/v1/versions/uploadChunk' \
-H 'Authorization: Bearer ${accessToken}' \
-F 'file=@/local/path/chunk-0;type=application/octet-stream' \
-F 'metadata={"uploadId":"${uploadId}","chunkIndex":0,"chunkSize":8388608,"chunkHash":"${sha256OfThisChunk}"};type=application/json'chunkIndex—— 0-based 序号;chunkSize—— 本片字节数(最后一片可小于 init 声明的 chunkSize);chunkHash—— 本片 SHA-256 hex(服务端校验)。
响应(UploadChunkResultDTO):
json
{ "code": 0, "data": { "chunkId": "C1000001", "chunkIndex": 0, "success": true } }第 3 段:完成(POST /v1/versions/completeChunkedUpload)
只传 uploadId——totalChunks / expectedContentHash 等在 init 阶段已持久化,complete 只信服务端值:
bash
curl -X POST 'https://api.atkonbase.example.com/v1/versions/completeChunkedUpload' \
-H 'Authorization: Bearer ${accessToken}' \
-H 'Content-Type: application/json' \
-d '{ "uploadId": "${uploadId}" }'服务端做:校验 [0, totalChunks) 全部到齐 → 按序合并、算整文件 SHA-256 → 若 init 给了 expectedContentHash 则比对 → blob 去重 → 建版本 → 清理分片。响应结构同 multipart 单传(FileUploadResultDTO)。
断点续传与幂等键设计
- 断点续传:网络中断后用同一
idempotencyKey重新initChunkedUpload,从响应的uploadedChunkIndexes得知已传分片,只补传缺失的,再complete。 idempotencyKey设计建议:应含userId+ targetContext(docId 或 containerId+title)+ 文件指纹(如整文件 hash),保证「同一用户上传同一文件到同一目标」稳定命中、不同上传不串。- ⚠️ 会话超期:上传会话默认 24 小时后被清理;超期后
uploadChunk/complete报「会话不存在」,需重新init。
三、预签名直传(直连对象存储)
字节不经应用进程中转,由客户端直接 PUT 到对象存储,再回调确认建版。三步走:
第 1 段:请求预签名 URL(POST /v1/versions/presign-upload)
bash
curl -X POST 'https://api.atkonbase.example.com/v1/versions/presign-upload' \
-H 'Authorization: Bearer ${accessToken}' \
-H 'Content-Type: application/json' \
-d '{
"containerId": "${containerId}",
"title": "report.pdf",
"originalFilename": "report.pdf",
"sizeBytes": 10485760,
"mimeType": "application/pdf",
"idempotencyKey": "${idempotencyKey}"
}'- 必填:
originalFilename/sizeBytes/mimeType。新建文档传containerId+title(可选documentTypeId/metadata/comment/label),已有文档追加新版本传docId。 sizeBytes是客户端声明值,finalize 时与对象存储实测大小比对,不一致即拒绝建版。
响应(PresignUploadResultDTO):
json
{
"code": 0,
"data": {
"mode": "presigned",
"uploadId": "U1000001",
"presignedUrl": "https://<bucket>.<endpoint>/<objectKey>?X-Amz-Signature=...",
"expiresAt": "2026-05-26T07:15:00Z",
"httpMethod": "PUT",
"signedHeaders": ["Content-Type"]
}
}mode 取值 | 含义 |
|---|---|
presigned | 已签发直传 URL,按下面第 2 / 3 段直传 |
fallback | 当前存储后端不支持预签名;改用 data.fallbackEndpoint(即 /v1/versions/upload,见一、multipart 单传),无需分支逻辑 |
already_completed | 同 idempotencyKey 已完成,直接复用 uploadId 对应的版本,无需再传 |
第 2 段:PUT 字节到对象存储
拿 presignedUrl 直接 PUT 文件字节——这一步不经过 ATKONBASE,直达对象存储。必须按 signedHeaders 携带与签名锁定一致的请求头(首版即 Content-Type,须等于 presign 时声明的 mimeType):
bash
curl -X PUT '${presignedUrl}' \
-H 'Content-Type: application/pdf' \
--data-binary '@/local/path/report.pdf'- 必须在
expiresAt之前完成;过期后该 URL 失效,需重新 presign。 Content-Type与 presign 声明不一致会被对象存储按签名校验拒收。
第 3 段:确认建版(POST /v1/versions/finalize-upload)
直传成功后,用第 1 段返回的 uploadId 回调:
bash
curl -X POST 'https://api.atkonbase.example.com/v1/versions/finalize-upload' \
-H 'Authorization: Bearer ${accessToken}' \
-H 'Content-Type: application/json' \
-d '{ "uploadId": "${uploadId}" }'- 服务端校验对象已就位、实测大小与
sizeBytes一致、配额未超、具备资源访问权限,全部通过才建版。响应结构同 multipart 单传(FileUploadResultDTO:docId/versionId/versionNo…)。 - 不调 finalize 不建版:仅 PUT 到对象存储而不回调 finalize,不会产生 Document / Version;未在有效期内 finalize 的会话与残留对象由平台自动回收。
四、限额与限制
| 限制 | 默认 / 上限 | 说明 |
|---|---|---|
| 单文件大小 | 默认 500MB | 单文件大小上限,默认 500MB,可由租户管理员调整 |
| 单分片大小 | 50MB(硬上限) | 不可配,分片大小须 ≤ 此值 |
| MIME 类型 | 黑 / 白名单 | 黑名单命中即拒;白名单非空时文件类型必须在白名单内 |
⚠️ 超限不是 HTTP 413:单文件超限 / MIME 不合规由上传校验抛业务错——HTTP 仍 200,
body.code != 0(多为code=1通用错),msg携带具体原因(如「文件大小超出限制」)。呼应入门篇 §错误处理:V1 不靠 HTTP status 判业务错,必须读body.code。此外,传输层还有一道独立的单请求体积上限;超过它会直接返回 HTTP 413(而非 200 业务错),这与上面的业务限额是两回事,413 归错误码参考的「非业务码的 HTTP 错误」附录口径,不要与业务限额混判。
五、上传与 Document / Version / Blob 的关系
- blob 按
contentHash去重:上传的字节按整文件 SHA-256 寻址,命中已存在且就绪的 blob 时复用同一blobId(省存储),不重复落字节。 - 但总是新建独立 Version:即便字节完全相同(blobId 复用),每次上传都产生一个新版本——
versionNo递增,comment/label各自独立。去重是存储层优化,不影响版本的独立性:两个版本指向同一 blob,仍是两条独立版本记录。 - 举例:把同一份文件上传到文档两次 → 两个 Version(versionNo 1、2),但
blobId相同。 - 上传链路的存储一致性由系统内部保证,集成方无需感知或特殊处理;底层存储的清理与回收不在 V1 对外范围。
六、内容直读与预览 URL(signed-url)
下载侧的对应能力:用版本 ID 换一个短时直读 URL,客户端拿到后直接 GET 取字节,同样可由对象存储直接出字节、不经应用进程中转。
bash
curl -X POST 'https://api.atkonbase.example.com/v1/versions/signed-url' \
-H 'Authorization: Bearer ${accessToken}' \
-H 'Content-Type: application/json' \
-d '{
"versionId": "V1000001",
"mode": "download",
"expiresInSec": 300
}'响应(SignedUrlResultDTO):
json
{
"code": 0,
"data": {
"mode": "presigned",
"url": "https://<bucket>.<endpoint>/<objectKey>?...",
"expiresAt": "2026-05-26T07:05:00Z",
"versionId": "V1000001"
}
}mode=download:URL 以 attachment 形态触发下载(Content-Disposition: attachment)。mode=preview:URL 以 inline 形态响应(Content-Disposition: inline+ 实际 MIME 类型),浏览器可直接在新标签页渲染图片 / PDF / 纯文本 / 音视频,无需先下载。expiresInSec控制有效期,缺省 300 秒、上限 3600 秒(1 小时)。- ⚠️ 预览 MIME 白名单:
mode=preview仅对image/*、application/pdf、text/plain、audio/*、video/*签发;出于安全考虑image/svg+xml不在白名单内。命中白名单之外的类型时端点返回错误,请改用mode=download。 - 响应
data.mode(presigned/fallback)是分发形态差异:presigned形态的 URL 在有效期内可多次 GET,fallback形态为一次性 URL;两种形态客户端都只需拿url直接 GET。
常见坑
- ⚠️ metadata part 漏
;type=application/json:被当普通字符串、解析失败。规避:multipart 的 metadata part 始终显式标 JSON content-type。 - ⚠️ 预签名直传漏 finalize:只把字节 PUT 到对象存储、没调
finalize-upload,永远不会建版。规避:PUT 成功后必须 finalize 才算上传完成。 - ⚠️ PUT 时
Content-Type与 presign 声明不一致:对象存储按签名校验拒收。规避:PUT 的Content-Type严格等于 presign 时声明的mimeType。 - ⚠️ 分片漏 complete:传完所有分片但没调
completeChunkedUpload,版本永远不生成。规避:所有分片确认后必须 complete。 - ⚠️ 分片缺号:
[0, totalChunks)有缺口时 complete 失败。规避:complete 前用 init 返回的uploadedChunkIndexes核对齐全。 - ⚠️ chunkHash / chunkSize 不符:分片字节与声明的 hash / 大小对不上,服务端校验拒收。规避:客户端按实际分片字节算 hash 与 size。
- ⚠️ 单片超 50MB:分片大小超硬上限被拒(
code=1,msg「单分片不能超过 50MB」)。规避:chunkSize ≤ 50MB。 - ⚠️ 大文件超时:单片过大或网关 timeout 偏小导致超时。规避:调小分片、或调大网关 / 客户端 timeout。
- ⚠️ 幂等键冲突:同
idempotencyKey但 targetContext 不一致(如换了目标容器却复用 key)→ 冲突。规避:key 绑定 targetContext,目标变则 key 变。 - ⚠️ 会话超期:超 24h 后续传报「会话不存在」。规避:长传尽快续、超期重新 init。
- 并发安全:分片并发上传的会话锁由服务端处理,客户端无需额外加锁。
下一步
- 上传后给文档授权 → ACL 与继承模式篇
- 上传时挂元数据字段、字段值怎么编码 → 字段值编码契约
- 上传相关错误响应 → 错误码参考