持久上传、断点摘要与提交回执
新 APP 优先使用 /api/v1/sync-client/v1 门面。实现已加入当前源码;接口随NAS发布版本安装,实际部署与硬件验收以运行时状态为准。
它复用现有 file_transfer 的 SQLite、2 MiB 持久块、完整摘要校验与文件身份代理;不会建立第二份独立字节存储。本文描述新版接口,安装了旧单文件协议的 NAS 不一定已有这些能力,连接后先查询 capabilities。
HTTP 门面与能力协商
GET /api/v1/sync-client/v1/capabilities
Authorization: Bearer <NAS token>
POST /api/v1/sync-client/v1/rpc
Authorization: Bearer <NAS token>
Content-Type: application/json
{"operation":"capabilities","params":{}}
HTTP 返回 {data:<结果>}。capabilities 当前声明 apiVersion 为 1.1,transport 为 nas-direct,cloudFileRelay:false。关键字段包括 upload.maxFileBytes:68719476736、chunkSize:2097152、fragmentSize:16384、resume:true、persistentReceipts:true、overwrite:false、zeroByteFiles:true;download.chunkBytes:16384、encoding:base64、resumeValidator:Last-Modified HTTP date、immutableSnapshot:false;directoryPageSize:32、maxRpcRequestBytes:32768。upload.stagingAvailable表示实际传输数据卷是否就绪,为false时先处理存储配置,不把支持resume理解为当前可写。读取能力不意味着账号拥有所有操作权限,真正调用仍检查原能力和文件ACL。完整上传工作流需要transfer.read/write及files.read/write:创建还要枚举目标目录,后续需要读取任务状态;不要只授予写入而假定所有查询都自动允许。
operation 为显式白名单,不接受任意 HTTP method、URL、文件系统绝对路径或客户端伪造的身份。params 拒绝未知字段,publicationTicket 只能在 NAS 内部产生,APP 不提供。HTTP 使用服务端固定 nas-app 上下文;直连使用客户端身份与批准目录绑定的上下文,两者任务不会自动混为一组。
上传操作字段
| operation | params | 成功结果 |
|---|---|---|
| uploads.create | requestId、root、path、name、size、sha256 | 完整上传 manifest |
| uploads.list | offset默认0、limit默认16(1–32) | items、nextOffset及传输能力;offset最大512 |
| uploads.get | id | manifest,加易失 fragmentReceived |
| uploads.action | id、action(pause/resume/cancel) | 操作后的 manifest |
| uploads.segment | id、offset、data(标准 base64)、chunkSha256 | manifest 加当前内存分片进度 |
| uploads.commit | id | 当前 manifest;可能先为 verifying,最终须检查 committed/receipt |
| uploads.forget | id、confirm:true | id、forgotten:true、destinationFileDeleted:false |
requestId 为 32 个小写十六进制字符,在第一次请求前由客户端随机生成并持久保存。同一账号、客户端上下文和 requestId 绑定不可变 root/path/name/size/sha256;重复 create 返回同一任务,改变目标或内容返回 sync_upload_request_conflict。不要以当前云 sessionId 代替 requestId,也不要在重试时生成新ID,否则会重复占用任务与空间。
sha256 是完整文件 64 位小写十六进制摘要,新门面用它绑定底层 fingerprint;与旧浏览器前后采样 fingerprint 不同,不能互换。size 为 0–68719476736。空文件的完整摘要为SHA256(empty),无需发送块,直接commit并按相同回执规则确认。root 为真实 root 标识,最长128个ASCII字母/数字/短横线/下划线;path 是目标已有目录的相对路径,最长4096,不含开头斜线、父目录或点路径片段;根目录用空字符串。name 是单个文件名,不能以点开头或含 /、反斜杠、冒号、控制字符,UTF-8长度不超过240字节。目录应先创建,不能在 name 中夹带层级。
uploads.list按持久化摘要分页,items序列化总量还受40KiB限制,可能少于limit;只根据nextOffset继续,null表示结束。列表不刷新每条底层校验状态,活跃任务使用uploads.get取得当前状态。
manifest 字段:id、requestId、root、path、name、size、sha256、status、received、prefixDigest、chunkSize、maxFileBytes、error、receipt、updatedAt。updatedAt 和回执 publishedAt 使用 Unix 毫秒;cloud 签名使用秒。receipt 未完成时为 null。id 是服务端任务标识,不能自行用 requestId 替代。
原生 HTTP 2 MiB 块
PUT /api/v1/sync-client/v1/uploads/<id>/chunks/<offset> Authorization: Bearer <NAS token> Content-Type: application/octet-stream X-Chunk-SHA256: <本块64hex> <原始字节>
offset 必须是服务器 received,且按 2097152 对齐。每块恰为 min(2097152,size-offset) 字节,最后一块可以更短。块写入、sync 和记录确认后 received 才前进。只需保持一个顺序写入队列,可预读下一块,但不要并发乱序 PUT。同一旧 offset 的相同块可以幂等核对;不同字节、偏移跳跃、块大小不符会被拒绝。
每块前缀链算法为:
C0 = SHA256(empty) Ci = SHA256(hexDecode(Ci-1) || SHA256(chunkBytes) || uint64BE(chunkByteLength))
SHA256(chunkBytes) 在拼接中是32个原始字节,不是64字符hex文本;长度是8字节大端无符号整数。服务器返回 prefixDigest 为Ci的小写hex。恢复时重算本地已确认前缀、核对链并恢复完整SHA累计状态。网络错误后先 GET,若 received 已前进且 prefixDigest匹配则接受已提交块;否则只在当前断点重试。绝不能直接相信客户端进度条或套用旧缓存值。
WebRTC 16 KiB 分片聚合
WebRTC 使用 uploads.segment,经控制通道每次发送最多16384原始字节的标准base64。offset 是绝对文件偏移;chunkSha256 是整个当前2MiB持久块的摘要,不是该16KiB片段摘要。一个块的所有segment带同一 chunkSha256,最后一个块按实际长度计算。
服务器按顺序在内存聚合,凑满该块才调用同一持久块引擎。响应中的 fragmentReceived 只是当前内存接收位置,received 才是可跨断线/重启依赖的断点。内存聚合最多8个任务,120秒后可被清理;暂停/取消也清理。同一片段重复时必须字节一致,不能跳跃。发生冲突或网络恢复时先查询状态,以权威received为基线重新开始当前未持久块,不能把fragmentReceived永久写成断点。
完成、回执与不确定状态
发送全部字节后调用 uploads.commit。它先触发底层完整文件SHA校验;可能返回 verifying,APP轮询 uploads.get 并在ready后再次commit。ready仅表示缓存可发布,不代表用户目录已有文件。
最终提交会重新检查NAS会话、能力、目录权限与批准范围,禁止覆盖现有文件。服务端先持久化committing,再通过原文件身份代理发布并保存receipt,成功状态为committed。只在 receipt 的 uploadId、requestId、root、path、name、size、sha256 与预期一致时标记成功:
{"uploadId":"<id>","requestId":"<requestId>","root":"<root>","path":"<实际已发布相对文件路径>","name":"<文件名>","size":123,"sha256":"<64hex>","publishedAt":1792108800000,"noOverwrite":true}
已committed再次commit返回原回执,不重复发布。若落盘完成但可靠回执无法确认,返回/记录commit_uncertain;客户端保留原件,提示人工核对目标,不自动再提交、覆盖或删除原文件。明确拒绝可为commit_failed,但同名冲突也不能推断原文件属于这个任务。
常见状态:created、accepted、uploading、paused、verifying、verification_failed、ready、committing、committed、cancelled、commit_failed、commit_uncertain。底层源导出还可能出现importing。暂停/恢复/取消只作用允许状态;完成与不确定任务不能当普通待上传任务恢复。
资源与错误处理
先把committed回执保存到APP自己的任务库,再在适合的时候uploads.forget清理服务端记录与缓存。forget仅允许committed/cancelled且明确confirm:true,不删除最终NAS文件;不能forget不确定提交来掩盖结果。清理后get返回404、原requestId不再保证原receipt可查询,客户端保留本地回执且不要重新创建同一次已成功任务。
底层单文件和账号总预留上限64GiB,需另留1GiB空闲;底层每账号最多512记录、每发送者最多16未完成任务,新manifest也有每账号512记录限制。最终发布期间缓存和目标文件可能同时占空间,64GiB配额不代表目标盘够用。配置的传输数据卷不在线时必须失败,不回落到系统盘。
| HTTP/类别 | 典型错误 | APP动作 |
|---|---|---|
| 400 | sync_client_invalid_request、sync_upload_invalid_request | 修正字段、大小、格式,不盲重试 |
| 401 | unauthorized | 重新验证NAS账号;保持本地任务与原件 |
| 403 | permission_denied、sync_scope_denied、sync_upload_destination_unavailable | 检查授权、范围和ACL,不自行提权 |
| 404 | sync_upload_not_found、sync_client_operation_not_found | 检查上下文/版本/任务ID |
| 409 | sync_upload_request_conflict、sync_upload_not_uploading、sync_upload_fragment_conflict、sync_upload_incomplete | GET当前状态并按错误分类恢复 |
| 409 | sync_upload_commit_uncertain | 停止自动提交,核对实际目标与回执 |
| 429 | sync_upload_record_limit、sync_upload_fragment_limit、底层配额拒绝 | 限制并发、维护任务、退避 |
| 503/507 | sync_transfer_unavailable、transfer_storage_unavailable、空间不足 | 检查服务/数据卷/容量,保留状态 |
下游某些拒绝会被门面折叠为sync_transfer_rejected,须同时检查HTTP状态与manifest.error,不能假设所有底层错误逐字透传。不要把未识别错误文案用于业务分支,保留code与请求ID供脱敏诊断。
HTTP 最小联调顺序
curl --fail-with-body --silent --show-error -H "Authorization: Bearer $NAS_TOKEN" -H 'Content-Type: application/json' --data-binary @upload-create.json "$NAS_BASE/api/v1/sync-client/v1/rpc" curl --fail-with-body --silent --show-error -X PUT -H "Authorization: Bearer $NAS_TOKEN" -H 'Content-Type: application/octet-stream' -H "X-Chunk-SHA256: $CHUNK_SHA256" --data-binary @chunk.bin "$NAS_BASE/api/v1/sync-client/v1/uploads/$UPLOAD_ID/chunks/0"
upload-create.json 是operation为uploads.create、params为上述创建字段的完整对象。原始文件超过一块时按权威received循环,然后用operation uploads.commit与params {id:…}完成。密钥只在本地安全环境提供;不要将真实登录文件、token或上传内容写进共享测试报告。