跳转到内容
AsterDrive Developer Docs开发者

上传完成契约矩阵

本文档记录 AsterDrive 当前上传链路的完成契约。公开文件上传 API 的非空文件统一从 /files/upload/init 创建 upload session:先由 upload::plan 固化文件名、MIME、大小、placement、policy 和 transport,再由 stream body、chunk、presigned 或 provider-resumable 数据面写入,最后在上传服务内完成收口。旧的普通 HTTP multipart 上传入口已经移除。后文矩阵中的 regular multipart/server path、local direct 和 streaming direct 是 upload/workspace storage 内部数据面,没有独立公开 HTTP 入口;session body 可以在 Init 后委托给 streaming direct 内部路径。

最终落文件时必须保持三个不变量:

  • 正式文件、blob/version、配额和 upload session 状态不能互相脱节。
  • 实际计费大小必须来自当前路径能信任的最终字节来源,而不是只信任客户端声明。
  • 已写入但未完成 DB 收口的对象要有明确 cleanup 或孤儿回收归属。
锚点当前职责备注
storage::store_from_temp_with_hints从服务端临时文件创建或覆盖文件;可走本地 dedup 或 non-dedup preuploaded blobLocal staged、WebDAV 和其他明确需要临时文件的内部写入使用
storage::store_preuploaded_nondedup从已经写入 driver 的 non-dedup blob 创建或覆盖文件session streaming direct 会落到这里
storage_core::finalize_upload_session_blob_with_actor_username在一个 DB 边界里创建文件、更新配额、把 session 标记 completedlocal chunked、stream relay chunked 直接使用
storage_core::finalize_upload_session_file为 opaque object 找到或创建 blob,再调用 session finalize,并发布 storage change eventpresigned single、presigned object multipart、relay object multipart、provider resumable 使用
upload::shared::run_upload_completion_stagecomplete 前把 session 从 expected status 切到 assembling;失败后按错误类型恢复或标 failed所有 upload session complete 路径共享

src/services/files/upload/complete/contract.rs 定义 upload-session complete 阶段本地使用的 VerifiedUploadedBlob。所有 session 型 complete 路径在进入 DB finalization 前,必须先把当前 transport 已经验证过的最终对象表达成这个类型。

该类型显式携带:

  • size:已验证的逻辑计费字节数。
  • policy_id:最终 blob 所属的 storage policy。
  • storage_path:已经写入或已经 complete 的对象路径。
  • source:content-addressed dedup、opaque object 或 preuploaded non-dedup blob。
  • cleanup:DB finalize 失败后要删除对象、清理 preuploaded blob、保留给 orphan GC,还是保留已完成 multipart object。

当前 VerifiedUploadedBlob 覆盖 presigned single、presigned object multipart、relay object multipart、provider direct/relay resumable、local chunked 和 stream relay chunked。

src/services/workspace/storage/store/contract.rs 定义 store_from_temp 路径使用的 VerifiedTempStoreBlob,覆盖 Local staged、WebDAV 和其他明确以临时文件进入 store_from_temp_with_hints 的落账契约。它把 content-addressed dedup、preuploaded non-dedup、staged dedup rollback、preuploaded cleanup 这些以前散在 persist.rs 里的约定集中起来。

storage::store_preuploaded_nondedup 使用本地 VerifiedPreuploadedNondedupStoreBlob 覆盖 streaming direct 的最终落账契约,校验 verified size、policy、storage path 和 prepared blob 一致后再进入 DB finalization。

容量观测与操作判断分层表达:

  • StorageCapacityStatus 是 driver、管理 API 和 Remote wire contract 返回的一次观测状态:supported 表示有可靠观测,unsupported 表示 connector 没有可移植容量接口,unavailable 表示本应可观测但本次没有得到可用数据。
  • StorageCapacityAssessment 使用声明大小评估一次观测,得到 sufficient、insufficient、unsupported 或 unavailable。迁移与上传必须复用同一判断,不各自解释 available_bytes。
  • upload planner 对明确不足或暂不可用的候选 target 增加本次请求的动态 exclusion,再按原 placement 规则选择后续 target;最终 policy、transport 和 session kind 只固化一次。
  • unsupported 是合法能力结果,上传继续并依赖实际数据面结果;unavailable 没有容量结论,优先回退其他 target,无候选时返回可重试错误。
  • 容量观测是 fast-fail 快照,不是跨请求 reservation。workspace quota 最终由事务中的 SQL CAS 保证,目标容量仍由 driver 写入结果和现有 cleanup/finalize 契约兜底。

容量探测使用 DriverRegistry 所有的请求驱动协调器,不运行周期扫描。协调器缓存原始 observation 而不是特定文件大小的 assessment;同 policy probe 使用 singleflight,跨 policy probe 受全局并发限制。每个 driver 提供 StorageCapacityProbePolicy:Local 使用 fresh 2 秒、充足值 stale 30 秒、negative 250 毫秒和固定 2 秒 timeout;OneDrive 与 Remote 使用 fresh 30 秒、充足值 stale 5 分钟、negative 1 秒,并允许 connector 将 timeout 配置为 2 至 30 秒(默认 10 秒)。stale 不足或不可用值先刷新确认,避免旧低水位误报。刷新失败时保留最后一个可用 observation,小请求可以继续使用 stale 充足值,而该 observation 对更大请求显示不足时返回最新探测错误。policy/credential/driver 失效会同步清除对应 observation,probe 任务独立于 HTTP 请求取消并记录失败与时延。

OffsetStaging / StreamStaging 在 session Init 返回前,对 upload_temp_dir 所在文件系统执行串行容量准入,并通过固定 revision 的 aster-fs allocation 契约预留完整 total_size 的物理块;单纯 set_len 形成的稀疏文件不再视为 reservation。Apple 文件系统上的新 extent 使用 all-or-nothing allocation,并在扩展逻辑 EOF 前校验内核返回的实际分配字节数;恢复已有 sparse session 时按真实 hole 补足物理块,不改变已有数据,也不会把不完整恢复误报为成功。准入保证分配后仍保留 server.upload_temp_min_free_bytes(默认 256 MiB)的 safety floor,恰好满足 required + floor 时允许,空间不足返回稳定的 upload.staging_capacity_insufficient(HTTP 507),并清理 session、临时目录和 Init 创建的相对路径目录。0 可关闭 safety floor,但不会关闭物理预分配。

cluster profile 下允许使用同一套 staging 协议,但部署者必须让所有 Primary 的 upload_temp_dir 指向同一个共享文件系统。Durable receipt、received_count、completion 状态与 assembly lease 仍以共享 writer database 为权威;同一 chunk 写入的排他性依赖部署文件系统提供跨实例 advisory lock。AsterDrive 不根据路径字符串推断 mount identity,也不建立第二套 shared-filesystem session kind。

reservation coordinator 通过共享 staging 文件系统串行化容量检查和分配,不建立重复的数据库 ledger;upload_sessions.session_kind + total_size + status 是重启恢复的 durable 事实源。进程首次 Init、Chunk PUT 或 Complete 触达 staging 时,会读取 active staged session,并按文件当前 allocated_size 只补足缺失物理块。成功 Complete、Cancel、过期清理和强制 policy cleanup 删除 session 临时目录时,文件系统同时释放 reservation;cluster 部署中的任一 Primary 都可对同一共享 session 目录执行这些操作。

所有 reverse follower 和 streaming direct 写入都以 StreamUploadAttempt 表达一次有 owner 的写入尝试。attempt 包含唯一 ID、正式目标路径、独立 staging 路径和 declared size;正式目标只在 payload 完整、实际大小匹配且 driver 提交成功后可见。

  • stage_attempt 只消费当前 attempt 的 reader,driver 负责保持 bounded streaming 和 declared-size 校验;commit_attempt 只在 relay 完整确认后发布正式目标,上层不再通过 exists 快照推断是否可以删除正式 key。AsterDrive 预分配的 opaque UUID object key 本身就是最终 blob identity,因此 S3-compatible 和 Remote 通过 provider 原子 PUT 直接写该 key,commit 不复制对象;Azure 通过 attempt-scoped block staging + commit block list 写入该 key,未提交 block 在 commit 前不可见并由 provider expiry 或 attempt-scoped abort 回收;Local 使用同一存储根内的 staging file + rename;SFTP 在目标已存在时要求 posix-rename@openssh.com。
  • abort_attempt 只清理当前 attempt 的 staging/session/未提交 parts,返回 NotRequired、Cleaned、Deferred 或 Unknown。NotRequired 表示 provider-atomic attempt 没有独立清理资源;Deferred / Unknown 表示清理状态需要后续维护确认。清理失败保留原始写入错误,并进入可重试的观测/维护路径。
  • 默认的 provider-atomic driver 可以直接把完整请求提交到目标 key;需要 staging 的 driver 必须使用 attempt 独立 namespace。多次同 key attempt 不得共享临时路径、multipart block、provider session 或清理权限。
  • relay、request cancellation、heartbeat timeout 和 process shutdown 都必须进入同一 abort 生命周期;有界 cleanup timeout 不得阻塞数据面,也不得读取完整对象来判断归属。
  • follower PUT 与 compose 的 stage 阶段使用 15 分钟有界超时;超时会释放当前 attempt、记录 stream_upload cleanup outcome,并保留 provider/session 的 Deferred 状态供后续维护重试。
  • 强制进程终止后的恢复以现有 DB 引用和维护链为事实源:已提交但尚未被 file_blob / file_revision 引用的 opaque object 由 blob_maintenance 的 orphan cleanup 重新核算引用后清理;Local/SFTP staging 由对应临时对象维护路径处理;Azure uncommitted blocks 和 OneDrive upload session 由 provider 的 abort/expiry 语义回收。这里不新增一张与 blob/file 引用平行的 attempt registry,避免两个 durable 状态源分叉。

attempt 重构保持现有流式背压模型:relay pipe、provider request body 和 hash wrapper 使用固定大小缓冲,不按对象总大小分配内存;multipart part 使用 provider 要求的最小分片和受控 buffer,不把完整对象聚合成 Vec<u8> 或 assembled 临时副本。S3-compatible、Azure 和 Remote 的普通 stream 不执行 provider-side 全对象 copy/compose;OneDrive 仅对不超过 1 MiB 的小请求保留 bounded simple-upload 快路径,更大的 stream 一律走 provider upload session。并发 attempt 通过 provider/policy resource pool 限制 in-flight 数量,等待资源时不消费 request body。验收同时记录峰值 RSS、单请求内存、staging 磁盘峰值、网络额外字节、吞吐和尾延迟。

attempt namespace 不引入全局或 target-key mutex。不同 request、不同 upload session 和不同 multipart part 继续并行;只有同一个 provider session 明确要求顺序 range 或最终 commit 时才在该 attempt 内串行。并发测试使用 barrier 证明至少两个同目标 attempt 同时进入数据面,不能只用 join! 猜测并行发生。

运行时通过现有 MetricsRecorder 暴露 attempt started/commit/abort 状态、expected bytes 和 active attempt gauge;指标标签只使用稳定事件、状态和资源类别,不包含 object key、URL、token 或 provider 凭据。

新建的 server-managed chunked session 不再为每个 chunk 保存一份 payload,也不会在 Complete 阶段重新拼写一份完整文件。Init、Chunk PUT 和 Complete 共享以下目录契约:

<upload_temp_dir>/<upload_id>/
├── .offset-staging-v1 # 唯一内容载体,Init 时预分配到 total_size
├── .chunk_0.lock # 同一 chunk 的跨任务/进程排他锁
└── .chunk_1.lock # 其他 lock 文件按需创建

offset-staging 的本地 receipt 存在 upload_session_parts:

part_number = chunk_number + 1
etag = aster-drive-offset-staging-receipt-v1
size = expected_chunk_size

upload_sessions.session_kind 是所有可操作 session 的权威数据面字段。它必须是合法的非空值;Complete、Chunk PUT、Progress 和 lifecycle 都直接校验显式 kind,不再根据临时文件、policy transport 或 assembled 推断路径。

Init 根据 connector-owned PolicyUploadTransport 持久化执行计划,不根据 DriverType 猜路径。当前值包括:

session_kind数据面完成计划
offset_staging本地 .offset-staging-v1 + DB receipt本地 staging finalize
stream_stagingstaging file + connector stream relaystream relay finalize
provider_relay_multipart / remote_relay_multipartprovider multipart parts + DB ETagrelay multipart complete
provider_presigned_single / remote_presigned_singleprovider temp objectpresigned single complete
provider_presigned_multipart / remote_presigned_multipartprovider multipart partspresigned multipart complete
provider_direct_resumableprovider upload session(浏览器直传 range)provider resumable complete
provider_relay_resumableprovider upload session(服务端顺序流式转发 range)+ DB receiptprovider resumable complete

从 0.5.0 起 session_kind 为 NOT NULL。升级迁移遇到 null 或非法 kind 会直接失败并保留原行;部署方需要先清理这些过期 session。显式 kind 与 multipart 字段组合不一致时,接口返回 upload.session_corrupted,不会降级到另一条数据面。

同一 chunk 先取得 .chunk_N.lock,不同 chunk 使用不同锁,因此可以并行写各自 offset。取得锁后按下面的顺序提交:

  1. 在 .offset-staging-v1 的 chunk_number * chunk_size 位置完整写入 payload。
  2. 对 staging file 执行 sync_data,先保证内容持久化。
  3. 开启只包含数据库 SQL 的短 writer transaction。
  4. 向 upload_session_parts insert-only 登记本地 chunk receipt。这里复用 (upload_id, part_number) 唯一键;本地 receipt 使用保留的 offset-staging 标识作为 etag,object multipart 仍保存 provider ETag。
  5. 只有 receipt 首次插入时才增加 upload_sessions.received_count,然后提交 transaction。

收到重复 Chunk PUT 时仍会完整校验 payload 大小。若 receipt 已存在,服务端会 drain/忽略请求 body,校验 receipt 后直接返回当前进度,不覆盖已提交 range,也不重复计数。

中断位置可见状态重试行为
staging range 写入完成前receipt 缺失,range 可能部分写入在同一 offset 完整覆盖,不计数
staging sync_data 后、DB transaction 前durable range 存在,receipt 缺失重新完整覆盖,然后登记 receipt
DB receipt transaction 提交后客户端未收到响应receipt 和内容都存在重试校验请求大小,返回当前进度,不重写、不重复计数
receipt row 缺失但 range 仍完整receipt 缺失,received_count 可能滞后重试完整覆盖并补登记,只计一次
receipt row 损坏receipt 存在但 sentinel/size 不匹配Chunk PUT 和 Complete 明确报损坏,不静默覆盖
staging file 被截断receipt 可能完整但内容载体长度错误Complete 拒绝并保留失败状态,避免把短文件当成完整上传

Complete 必须同时校验:

  • upload_session_parts 中恰好有 total_chunks 条本地 receipt,part 序号连续,sentinel 和 size 与每个 chunk 一致;
  • .offset-staging-v1 是普通文件;
  • staging file 长度等于 session.total_size。

Local completion 直接消费这份 staging file:开启 content_dedup 时会先流式计算 SHA-256,再按 content-addressed key promote;关闭 dedup 时把同一 staging file 写入预分配的独立 Blob。两种情况都不会再完整写一份 assembled 文件。需要 generic stream upload 的 connector 从 staging file 串流到目标 driver。S3-compatible、Azure Blob、Tencent COS 等已经协商到 provider relay multipart 的 session 不走这条本地 staging 路径。

0.5.0 不读取或迁移 0.4.x 的 payload-per-chunk session,也不创建或复用 assembled。升级迁移会在发现 null/非法 session_kind 时停止,旧 session 的清理责任属于部署方。

OneDrive(Microsoft Graph)这类 provider 自己提供有状态、可查询进度的 upload session。Connector 可以选择两条数据面:FrontendDirect 把临时 upload URL 交给已认证浏览器;ServerRelay 只把 AsterDrive upload ID 和分片调度返回给浏览器,由 Primary 把请求体顺序流式转发到同一个 provider session。

  • Connector 必须声明 ProviderResumable(FrontendDirect | ServerRelay) transport,driver 必须暴露 provider_resumable;只有 direct 路径额外要求 frontend_direct_upload = true。
  • chunk_size 采用 provider 的 default_fragment_size,并校验 min/max/alignment;total_chunks 按它计算。所有 range 都必须顺序提交:direct 前端跟随 next_expected_ranges,relay 后端通过调度元数据和共享 DB 状态机限制并发乱序。
  • Init 调用 create_upload_session(object_temp_key) 创建 provider session。object_temp_key 由 storage policy 对应 connector descriptor 的 object_naming 能力生成:opaque_uuid 使用 files/{upload_id},original_filename 使用 files/{upload_id}/{normalized_filename}。OneDrive 属于后者,因此 Graph item 保留原始文件名;命名规则统一由 descriptor 能力提供,上传 service 仅消费解析结果。upload URL 本身等价于写凭据,因此加密存入 upload_sessions.provider_session_ciphertext:密钥用 auth.storage_credential_secret_key,AAD 为 upload_session:{upload_id}:provider_resumable。
  • Direct session 持久化为 provider_direct_resumable,响应包含临时 provider upload URL;relay session 持久化为 provider_relay_resumable,响应保持 chunked 模式,不暴露 upload URL,并声明 sequential / max_chunk_concurrency = 1。
  • expires_at 取 provider session 过期时间和默认 24 小时的较小值。
  • DB 持久化失败(包括 upload_id 冲突重试)时必须调用 abort_upload_session 并删除 object_temp_key,不允许泄露悬挂 session 或命名空间。

浏览器直接向 upload URL PUT 带 Content-Range 的 fragment,不带 AsterDrive 凭据。range 冲突(416)和瞬时失败由前端按 next_expected_ranges 重试。Provider 在最后一个 range 接收后隐式完成 session(implicit_completion),对象直接落在 object_temp_key,没有独立的 complete 请求发给 provider。

  • 浏览器只调用 AsterDrive 的 authenticated chunk PUT;Graph upload URL 始终加密保存在服务端。
  • Primary 使用固定 64 KiB duplex pipe,把 Actix request payload 直接接到 upload_session_fragment_reader,不会把完整分片落盘或整体缓存在内存。
  • Graph range PUT 必须带准确的 Content-Length / Content-Range,不能向预认证 upload URL 附加 OAuth header。OneDrive 非最终分片保持 320 KiB 对齐,单次请求不超过 50 MiB。
  • upload_session_parts 的 (upload_id, part_number) 唯一键承担共享数据库 claim;空 ETag 表示 active claim,provider-range-v1 表示 provider 已确认接受该 range。received_count 是下一段唯一合法的 chunk number,乱序请求必须拒绝。
  • 长时间 PUT 每 30 秒刷新 claim;超过 120 秒的 claim 也只能在 provider 仍期待相同起点时回收。多个 Primary 不依赖 sticky session 或共享本地临时目录。

Direct progress 解密 provider_session_ciphertext 后调用 query_upload_session,把 provider 返回的 next_expected_ranges 换算成已完成 chunk 列表。Relay progress 还会按 provider offset 顺序补齐缺失的 DB receipt 和 received_count。

Relay PUT 返回错误时不能直接假设失败:provider offset 已越过当前 range 就补写 receipt;仍等于 range 起点才释放 claim;落在 range 中间则标记 session corrupted;query 同时失败时保留 claim,等待后续请求对账,避免重复 PUT。provider session 返回 NotFound 时,用 object_temp_key 的存在性区分“尚未提交”与“最终 range 已隐式完成”。

  • Relay Complete 先再次按 provider 进度补 receipt,并要求 received_count == total_chunks;两条路径随后都读取 object_temp_key metadata,实际 size 必须等于 session.total_size。
  • verified blob 使用 VerifiedUploadedBlob::precommitted_provider_object:source 为 opaque object,cleanup 为 DeleteStorageObjectOnDbFailure,之后走 finalize_verified_opaque_upload_session -> finalize_upload_session_file,quota 在 DB 事务内原子落账。
  • 取消、过期或强制删除策略时先调用 provider abort,再删除 object_temp_key。provider session 已不存在视为清理完成;瞬时错误保留 session 等待重试,权限或配置错误保留给人工处理。
上传模式 / transport初始状态和写入位置trusted size sourcequota precheck / atomic chargefinalize functioncleanup / idempotency
regular multipart/server path不创建 upload session;upload_with_hints 读取 actix_multipart::Multipart 到 runtime temp file服务端读取 multipart body 时累计的 size;如有 declared_size,必须和累计值相等policy resolved by actual size;preuploaded non-dedup blob 会在对象写入前 precheck;DB 事务内再次 check_quota,再 update_storage_usedstore_from_temp_with_hints -> store::from_temp / persist_temp_store / write_file_record_from_temp请求临时文件在 store_from_temp_with_hints 返回后删除;preuploaded 对象在 DB 失败时 cleanup;dedup staged 对象只有在确认没有 blob row 引用时回滚,否则交给 orphan GC
local direct不创建 upload session;local policy 且有 declared_size 时直接写入 local staging path写入 local staging file 时累计的 size,必须等于 declared_size;dedup 时同流计算 hash使用已解析 local policy;和 server path 一样通过 store_from_temp_with_hints 做 precheck / 事务内 atomic chargeupload_local_direct -> store_from_temp_with_hints写入、大小不匹配、空文件或 store 结束后删除 staging file;重复请求不会通过 session 幂等,只按普通创建语义处理
streaming direct不创建 upload session;relay request body 到 driver 的 prepared non-dedup blobdriver metadata(storage_path).size,必须等于 declared_size,并再次检查 policy max file sizerelay 前先用 declared_size 做 quota precheck;metadata 复验后再用 actual_size precheck;DB 事务内再次 check_quota 并 update_storage_usedupload_streaming_direct -> store_preuploaded_nondedupstorage upload、relay、metadata、size validation、quota validation 或 DB finalize 失败时 cleanup prepared blob;成功后按正式 blob 管理
local chunked / offset stagingsession status uploading;Init 预创建 .offset-staging-v1,Chunk PUT 按 offset 写 range 并登记 DB receipt每块必须等于 expected_chunk_size_for_upload;Complete 校验全部 receipt 和 staging file 的 session.total_size;dedup 时从 staging 流式计算 SHA-256chunk receipt 与 received_count 在只含 SQL 的短 writer transaction 内幂等登记;最终 quota 仍由 finalize_upload_session_blob_with_actor_username 原子落账complete_chunked_upload_with_actor_username -> finalize_chunked_upload_session -> load_offset_staging_file -> stage_chunked_temp_file -> persist_chunked_uploadstaging range 先 sync_data;receipt 是唯一 completion index,唯一键避免重试重复计数;Complete 成功后删除 upload temp dir
presigned singlesession status presigned;客户端 PUT 到 object_temp_keycomplete 前读取 temp object metadata;copy 到 final key 后再次读取 final object metadata;两者都必须等于 session.total_sizecomplete 阶段没有独立 quota precheck;finalize_upload_session_file 在 DB 事务内创建 blob/file、atomic charge、标 completedcomplete_presigned_upload -> copy_presigned_object_to_final_key -> finalize_verified_opaque_upload_session -> finalize_upload_session_filetemp object 缺失或大小不匹配会失败,大小不匹配会尝试删除 temp object;DB finalize 失败后删除 copied final object;成功后 best-effort 删除 temp object;completed retry 通过 find_file_by_session 返回已有文件
presigned object multipartsession status presigned;客户端直传 object multipart parts,complete 时客户端回传 partsprovider list_uploaded_part_details 的 part size 求和,必须等于 session.total_size;multipart complete 后再读 object metadatamultipart complete 前先用 part size total check_quota;finalize_upload_session_file 在 DB 事务内 atomic charge、标 completedcomplete_presigned_multipart -> complete_object_multipart_upload_session -> finalize_verified_opaque_upload_session -> finalize_upload_session_filecompleted parts 和 provider uploaded parts 必须连续且数量匹配;preflight size/parts/quota 失败会 abort multipart;complete 出现 retryable storage error 且 object 已存在时继续 finalize;multipart object 一旦 complete,VerifiedUploadedBlob.cleanup = RetainCompletedMultipartObject,因此 finalize_upload_session_file/DB finalize 失败后不删除已完成对象,留给后续重试或 orphan cleanup;completed retry 返回已有文件
relay object multipartsession status uploading;每个 chunk 由服务端 relay 到 object multipart,并把 part metadata 写入 upload_session_partschunk 阶段按 expected_chunk_size_for_upload 验每个 payload;complete 阶段读取服务端 parts 清单,再用 provider part details 求和,必须等于 session.total_sizechunk 阶段不 charge;complete multipart 前用 verified part total precheck;finalize_upload_session_file 在 DB 事务内 atomic charge、标 completedcomplete_relay_multipart -> complete_object_multipart_upload_session -> finalize_verified_opaque_upload_session -> finalize_upload_session_filepart claim 防止同一 part 并发重复上传;upload 或 DB 写 part metadata 失败会 release claim;complete preflight 失败会 abort multipart;multipart object 一旦 complete,VerifiedUploadedBlob.cleanup = RetainCompletedMultipartObject,因此 finalize_upload_session_file/DB finalize 失败后不删除已完成对象,留给后续重试或 orphan cleanup;completed retry 返回已有文件
provider direct resumablesession status uploading;Init 创建 provider upload session,upload URL 加密存 provider_session_ciphertext;浏览器按 next_expected_ranges 顺序直传 range,不经过 AsterDriveprovider 隐式完成后读 object_temp_key metadata,必须等于 session.total_sizecomplete 阶段没有独立 quota precheck;finalize_upload_session_file 在 DB 事务内创建 blob/file、atomic charge、标 completedcomplete_provider_resumable_upload -> finalize_verified_opaque_upload_session -> finalize_upload_session_fileInit 持久化失败 abort provider session;取消/过期解密 ciphertext 后 provider abort;DB finalize 失败删除已提交对象;progress 用 provider query 恢复,session 404 时按对象存在性判断隐式完成;completed retry 返回已有文件
provider relay resumablesession status uploading;Init 创建 provider upload session,upload URL 只加密留在服务端;浏览器 chunk 由 Primary 顺序流式转发到 provider range PUT每个请求体必须等于 expected chunk size;provider nextExpectedRanges / 最终对象存在性是 range 是否提交的事实源;complete 再校验最终 metadata sizechunk claim、receipt 和 received_count 通过共享 writer DB 事务顺序推进;最终 quota 仍由 finalize_upload_session_file 原子落账provider_relay::upload_payload / upload_bytes -> complete_provider_resumable_upload -> finalize_verified_opaque_upload_session -> finalize_upload_session_fileDB 唯一 claim + heartbeat 防止多 Primary 重复 PUT;模糊响应先 query provider 再决定补 receipt、释放 claim或保留待对账;取消/过期先 abort provider session 再删临时对象;completed retry 返回已有文件
remote/follower upload transportsremote policy 通过 remote driver 暴露 direct、presigned、presigned multipart 或 relay multipart;session 状态和 object_temp_key / object_multipart_id 与对应 object-storage transport 相同direct relay 使用 streaming direct metadata;remote presigned single 使用 temp/final metadata;remote presigned multipart 和 remote relay multipart 使用 provider part details + final metadata与实际选择的 transport 相同;remote relay direct 走 store_preuploaded_nondedup,remote presigned / multipart 走 upload session finalizeinit_remote_upload 只选择 transport;完成阶段复用 upload_streaming_direct、complete_presigned_upload、complete_presigned_multipart 或 complete_relay_multipartcleanup/idempotency 继承实际 transport;remote/follower 的特殊性只在 driver/protocol 层,产品层不应新增一套平行 finalize 语义

upload::complete::plan::determine_completion_plan 使用已解析的 UploadSessionKind 选择完成计划;session 状态只负责幂等、assembling、过期和失败错误:

  • completed -> ReturnCompleted,通过 find_file_by_session 幂等返回已有文件,不应再次 charge quota。
  • provider_presigned_single / remote_presigned_single -> CompletePresigned。
  • provider_presigned_multipart / remote_presigned_multipart -> CompletePresignedMultipart,客户端必须提交 parts。
  • provider_relay_multipart / remote_relay_multipart -> CompleteRelayMultipart,parts 来自服务端已保存的 upload_session_parts。
  • provider_direct_resumable -> CompleteProviderResumable,以 provider 侧对象 metadata 为完成依据。
  • provider_relay_resumable -> 先按 provider 进度补齐服务端 receipt;全部 range 完成后进入 CompleteProviderResumable。
  • offset_staging / stream_staging -> CompleteChunked,要求 received_count == total_chunks。

run_upload_completion_stage 会先把 expected status 切到 assembling。非 retryable 失败会把 session 标为 failed;retryable storage error 会尝试恢复到原状态,允许客户端重试。

本文件只是当前契约基线,后续代码迁移仍需要完成这些 acceptance criteria:

  • session complete 路径继续使用 VerifiedUploadedBlob 或同等明确的 verified finalization input;新 complete 入口必须显式声明 verified size、policy、storage path/blob source 和 DB finalize failure cleanup plan。
  • store_from_temp 路径继续使用 VerifiedTempStoreBlob 或同等明确的 verified finalization input;新 temp-store 入口必须显式声明 staged dedup/preuploaded cleanup 责任。
  • store_preuploaded_nondedup 路径继续使用 VerifiedPreuploadedNondedupStoreBlob 或同等明确的 verified finalization input;新 preuploaded store 入口必须显式校验 prepared blob 的 size/policy/storage path 一致性。
  • 每个被迁移路径都要补 quota、size mismatch 和 DB finalize failure cleanup 测试;completed retry 不重复计费 只适用于 session complete flow。
  • offset-staging 改动必须覆盖:不同 chunk 确实并行、同一 chunk 确实排他、partial range 覆盖、receipt 缺失、receipt 损坏和 staging 截断。并发测试需要 barrier/failpoint 证明任务进入了关键区,不能只用 join! 假设发生过竞争。
  • 保持 public API request/response、session status 语义和现有成功上传行为不变。