Skip to content

文件上传链路模式篇

目标:让集成方在 multipart 单传、分片三段式与预签名直传之间正确选型、实现断点续传、用签名直读 / 预览 URL 取回内容、理解限额、看懂上传与 Document / Version / Blob 的关系(去重不影响版本独立),并避开链路坑。

不在本篇范围:版本列表 / 回退 / 保留策略(文档与版本模式篇);元数据字段值编码(见字段值编码契约);容器模型与层级(容器模式篇);租户存储用量 / 计费(多租户用量模式篇);错误码完整码表(见错误码参考)。

本篇是领域模型 §Version / Blob的纵深展开。multipart 最小上传见入门篇 §第一次上传

两条链路与选型

链路端点适用
multipart 单传POST /v1/versions/upload文件 < 500MB、网络稳定,一次请求传完
分片三段式initChunkedUploaduploadChunkcompleteChunkedUpload大文件、网络不稳,需断点续传 / 并发分片
预签名直传presign-upload → 客户端 PUT 至对象存储 → finalize-upload字节不经应用进程中转、直连对象存储,适合大附件 / 高并发;后端不支持预签名时自动回退到 multipart

选型阈值:单文件较小(默认 < 500MB)且网络稳定走 multipart;文件大、或网络不稳、或需要断点续传 / 并发上传时走分片(单分片硬上限 50MB,分片大小须 ≤ 50MB)。希望上传字节不经应用进程中转、直连对象存储以承载大附件与高并发时走预签名直传——当后端不支持预签名时,presign-upload 会返回 mode=fallback 指引你退回 multipart,无需分支代码。

一、multipart 单传

两个 part:fileapplication/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(目标文档 / 容器上下文)完全匹配时返回已有版本而非重复上传;上下文不匹配则视为冲突。
  • ⚠️ metadata part 的 ;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 —— 幂等命中:若同 idempotencyKeyCOMPLETED 且 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_completedidempotencyKey 已完成,直接复用 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 单传(FileUploadResultDTOdocId / versionId / versionNo …)。
  • 不调 finalize 不建版:仅 PUT 到对象存储而不回调 finalize,不会产生 Document / Version;未在有效期内 finalize 的会话与残留对象由平台自动回收。

四、限额与限制

限制默认 / 上限说明
单文件大小默认 500MB单文件大小上限,默认 500MB,可由租户管理员调整
单分片大小50MB(硬上限)不可配,分片大小须 ≤ 此值
MIME 类型黑 / 白名单黑名单命中即拒;白名单非空时文件类型必须在白名单内

⚠️ 超限不是 HTTP 413:单文件超限 / MIME 不合规由上传校验抛业务错——HTTP 仍 200body.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/pdftext/plainaudio/*video/* 签发;出于安全考虑 image/svg+xml 不在白名单内。命中白名单之外的类型时端点返回错误,请改用 mode=download
  • 响应 data.modepresigned / 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。
  • 并发安全:分片并发上传的会话锁由服务端处理,客户端无需额外加锁

下一步