Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Breaking

- **上传 session 0.5.0 边界** — `upload_sessions.session_kind` 收紧为 `NOT NULL`,并删除 0.4.x payload-per-chunk `chunk_N`、`assembled` 拼装/relay、kind 推断和 assembly limiter。升级迁移遇到 null 或非法 kind 会停止且保留原行;部署方需要先清理已过期的旧上传 session。

### Changed

- Chunk PUT、Progress、Complete、Cancel/Cleanup 只接受持久化的显式 `UploadSessionKind`,仍会校验 multipart、temp key 和 provider session metadata 的组合不变量。
- OffsetStaging、StreamStaging、provider/remote relay multipart、presigned 和 provider resumable 主路径保持不变;upload service 继续从 connector-owned transport 生成 Init plan,不引入 `DriverType` 分流矩阵。

## [v0.4.0] - 2026-07-23

### Release Highlights
Expand Down
4 changes: 2 additions & 2 deletions developer-docs/zh-CN/api/files.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,11 @@

当前新建的 server-managed chunked session 使用 offset staging:Init 在 session 临时目录预创建 `.offset-staging-v1`,每个 Chunk PUT 按 `chunk_number * chunk_size` 写入对应 range;`upload_session_parts` 中的本地 receipt 表示该 range 已 durable publish。新 session 不创建 `chunk_N` marker,Complete 也不会再次拼写整文件。

每个需要 session 的 Init 都会在 `upload_sessions.session_kind` 持久化 connector-owned 执行计划(例如 `offset_staging`、`stream_staging`、`provider_relay_multipart`、`provider_presigned_multipart`)。上传服务不把 `DriverType` 硬编码到 Chunk/Complete 路径;兼容旧 row 时才根据 policy transport 和独立 staging 路径恢复 kind。kind 与 multipart 字段不一致会返回 `upload.session_corrupted`,不会静默切换到本地 chunk 文件路径。
每个需要 session 的 Init 都会在 `upload_sessions.session_kind` 持久化 connector-owned 执行计划(例如 `offset_staging`、`stream_staging`、`provider_relay_multipart`、`provider_presigned_multipart`)。从 0.5.0 起该字段为非空强约束;上传服务不把 `DriverType` 硬编码到 Chunk/Complete 路径,也不为旧 row 推断 kind。kind 与 multipart 字段不一致会返回 `upload.session_corrupted`,不会静默切换到本地 chunk 文件路径。

本地 Chunk PUT 会先对 staging range 执行 `sync_data`,再在只含 SQL 的短 writer transaction 中 insert-only 登记 chunk receipt 并更新 `received_count`。客户端重试只校验已有 receipt,不会重写已提交 range,也不会重复计数。

升级前已经存在的 payload-per-chunk session 仍走 legacy compatibility path:这类 session 的 `chunk_N` 是实际 payload,Complete 可能创建 `assembled`。新旧格式通过 `.offset-staging-v1` 专用路径区分,不通过 `assembled` 是否存在判断;legacy `assembled` 即使在失败后残留,也仍按 legacy session 重试
0.5.0 不继续 payload-per-chunk session,也不创建或复用 `assembled`。升级时若数据库仍有 null/非法 `session_kind`,迁移会直接失败并保留原行,部署方需要先清理过期 session。

完成阶段的服务端行为分两类:

Expand Down
25 changes: 6 additions & 19 deletions developer-docs/zh-CN/design/upload-finalization-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
上传路径分成两组:

- 普通 HTTP multipart 上传:入口在 `storage::multipart`,直接在一次请求内落到正式文件。
- upload session 型上传:入口在 `upload::{init, chunk, complete}`,先持久化 `upload_session`,complete 阶段再把临时对象、分片或 assembled 文件收口成正式文件。
- upload session 型上传:入口在 `upload::{init, chunk, complete}`,先持久化 `upload_session`,complete 阶段再把临时对象或 staging 文件收口成正式文件。

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

Expand Down Expand Up @@ -60,7 +60,7 @@ etag = aster-drive-offset-staging-receipt-v1
size = expected_chunk_size
```

`.offset-staging-v1` 是旧 session 的兼容格式线索,但新 session 的权威格式字段是 `upload_sessions.session_kind`。Complete、Chunk PUT、Progress 和 lifecycle 先使用显式 kind;只有 `session_kind IS NULL` 的迁移前 row 才通过统一 compatibility classifier 读取 policy transport、multipart 字段和 `.offset-staging-v1`。legacy compatibility path 创建的通用 `assembled` 文件不参与判断。这个边界很重要:legacy 首次拼装后如果 storage/DB 阶段出现可重试失败,`assembled` 可能保留到下一次 Complete;若拿它判断格式,就会把 payload-sized `chunk_N` 错当成 offset receipt
`upload_sessions.session_kind` 是所有可操作 session 的权威数据面字段。它必须是合法的非空值;Complete、Chunk PUT、Progress 和 lifecycle 都直接校验显式 kind,不再根据临时文件、policy transport 或 `assembled` 推断路径

### 显式 session kind

Expand All @@ -74,9 +74,8 @@ Init 根据 connector-owned `PolicyUploadTransport` 持久化执行计划,不
| `provider_presigned_single` / `remote_presigned_single` | provider temp object | presigned single complete |
| `provider_presigned_multipart` / `remote_presigned_multipart` | provider multipart parts | presigned multipart complete |
| `provider_direct_resumable` | provider upload session(浏览器直传 range) | provider resumable complete |
| `legacy_chunk_files` | 迁移前 `chunk_N` payload | legacy assemble/relay compatibility |

`session_kind` 在 0.5.0 前保持 nullable,以便读取升级前的 session;新 Init 永远写入非空值。显式 kind 与 multipart 字段组合不一致时,接口返回 `upload.session_corrupted`,不会降级到另一条数据面。
0.5.0 起 `session_kind` 为 `NOT NULL`。升级迁移遇到 null 或非法 kind 会直接失败并保留原行;部署方需要先清理这些过期 session。显式 kind 与 multipart 字段组合不一致时,接口返回 `upload.session_corrupted`,不会降级到另一条数据面。

### Chunk PUT 的 durable receipt 顺序

Expand Down Expand Up @@ -109,18 +108,7 @@ Complete 必须同时校验:

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 路径。

### Legacy compatibility path

升级前创建的 session 仍可能采用 payload-per-chunk 目录:

```text
<upload_temp_dir>/<upload_id>/
├── chunk_0 # payload
├── chunk_1 # payload
└── assembled # Complete 拼装产物,失败/崩溃后可能保留
```

这条路径会在 Complete 时取得 `chunk_assembly_to_local_temp_file` limiter,然后拼装或串流 legacy chunk。它只保护 legacy assembly,不限制新 `.offset-staging-v1` session。兼容路径计划在 `0.5.0` 移除;移除前必须保留“已有 assembled 仍按 legacy 重试”的回归测试。
0.5.0 不读取或迁移 0.4.x 的 payload-per-chunk session,也不创建或复用 `assembled`。升级迁移会在发现 null/非法 `session_kind` 时停止,旧 session 的清理责任属于部署方。

## Provider direct resumable 契约

Expand Down Expand Up @@ -156,7 +144,6 @@ Progress 不解本地 receipt,而是解密 `provider_session_ciphertext` 后
| 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 charge | `upload_local_direct` -> `store_from_temp_with_hints` | 写入、大小不匹配、空文件或 store 结束后删除 staging file;重复请求不会通过 session 幂等,只按普通创建语义处理 |
| streaming direct | 不创建 upload session;relay request body 到 driver 的 prepared non-dedup blob | driver `metadata(storage_path).size`,必须等于 `declared_size`,并再次检查 policy max file size | relay 前先用 `declared_size` 做 quota precheck;metadata 复验后再用 `actual_size` precheck;DB 事务内再次 `check_quota` 并 `update_storage_used` | `upload_streaming_direct` -> `store_preuploaded_nondedup` | storage upload、relay、metadata、size validation、quota validation 或 DB finalize 失败时 cleanup prepared blob;成功后按正式 blob 管理 |
| local chunked / offset staging | session 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-256 | chunk 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_upload` | staging range 先 `sync_data`;receipt 是唯一 completion index,唯一键避免重试重复计数;Complete 成功后删除 upload temp dir |
| legacy local chunk files | 升级前 session 的 `chunk_N` 是 payload;Complete 拼写 `assembled`,generic stream connector 则依次读取 chunk | 拼装路径按实际读取字节累计 size;legacy stream 使用 session total size 作为声明值并由下游 storage contract 校验 | 最终 quota 与当前 chunked path 相同;legacy assembly limiter 只限制本地拼装写,不影响 offset staging | `assemble_legacy_local_chunks_to_temp_file` 或 `stream_legacy_local_chunks_into_writer` -> 当前 chunked persist/finalize | `assembled` 可能在 retryable storage/DB 失败后保留,但不能触发 offset-staging 判断;进入兼容路径会 warn,计划在 `0.5.0` 移除 |
| presigned single | session status `presigned`;客户端 PUT 到 `object_temp_key` | complete 前读取 temp object metadata;copy 到 final key 后再次读取 final object metadata;两者都必须等于 `session.total_size` | complete 阶段没有独立 quota precheck;`finalize_upload_session_file` 在 DB 事务内创建 blob/file、atomic charge、标 completed | `complete_presigned_upload` -> `copy_presigned_object_to_final_key` -> `finalize_verified_opaque_upload_session` -> `finalize_upload_session_file` | temp object 缺失或大小不匹配会失败,大小不匹配会尝试删除 temp object;DB finalize 失败后删除 copied final object;成功后 best-effort 删除 temp object;completed retry 通过 `find_file_by_session` 返回已有文件 |
| presigned object multipart | session status `presigned`;客户端直传 object multipart parts,complete 时客户端回传 parts | provider `list_uploaded_part_details` 的 part size 求和,必须等于 `session.total_size`;multipart complete 后再读 object metadata | multipart complete 前先用 part size total `check_quota`;`finalize_upload_session_file` 在 DB 事务内 atomic charge、标 completed | `complete_presigned_multipart` -> `complete_object_multipart_upload_session` -> `finalize_verified_opaque_upload_session` -> `finalize_upload_session_file` | completed 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 multipart | session status `uploading`;每个 chunk 由服务端 relay 到 object multipart,并把 part metadata 写入 `upload_session_parts` | chunk 阶段按 `expected_chunk_size_for_upload` 验每个 payload;complete 阶段读取服务端 parts 清单,再用 provider part details 求和,必须等于 `session.total_size` | chunk 阶段不 charge;complete multipart 前用 verified part total precheck;`finalize_upload_session_file` 在 DB 事务内 atomic charge、标 completed | `complete_relay_multipart` -> `complete_object_multipart_upload_session` -> `finalize_verified_opaque_upload_session` -> `finalize_upload_session_file` | part 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 返回已有文件 |
Expand All @@ -172,7 +159,7 @@ Progress 不解本地 receipt,而是解密 `provider_session_ciphertext` 后
- `provider_presigned_multipart` / `remote_presigned_multipart` -> `CompletePresignedMultipart`,客户端必须提交 parts。
- `provider_relay_multipart` / `remote_relay_multipart` -> `CompleteRelayMultipart`,parts 来自服务端已保存的 `upload_session_parts`。
- `provider_direct_resumable` -> `CompleteProviderResumable`,parts 不存在于服务端;以 provider 侧对象 metadata 为完成依据。
- `offset_staging` / `stream_staging` / `legacy_chunk_files` -> `CompleteChunked`,要求 `received_count == total_chunks`;只有 legacy null row 才需要 compatibility classifier
- `offset_staging` / `stream_staging` -> `CompleteChunked`,要求 `received_count == total_chunks`。

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

Expand All @@ -184,5 +171,5 @@ Progress 不解本地 receipt,而是解密 `provider_session_ciphertext` 后
- `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 截断和 legacy assembled 残留重试。并发测试需要 barrier/failpoint 证明任务进入了关键区,不能只用 `join!` 假设发生过竞争。
- offset-staging 改动必须覆盖:不同 chunk 确实并行、同一 chunk 确实排他、partial range 覆盖、receipt 缺失、receipt 损坏和 staging 截断。并发测试需要 barrier/failpoint 证明任务进入了关键区,不能只用 `join!` 假设发生过竞争。
- 保持 public API request/response、session status 语义和现有成功上传行为不变。
8 changes: 3 additions & 5 deletions frontend-panel/src/services/api.generated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions migration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ mod m20260713_000003_scheduled_tasks;
mod m20260716_000001_bind_external_auth_login_flows;
mod m20260717_000001_add_upload_session_kind;
mod m20260719_000001_add_upload_provider_session;
mod m20260723_000001_require_upload_session_kind;
mod search_acceleration;
mod time;

Expand Down Expand Up @@ -179,6 +180,7 @@ impl MigratorTrait for CurrentMigrator {
Box::new(m20260716_000001_bind_external_auth_login_flows::Migration),
Box::new(m20260717_000001_add_upload_session_kind::Migration),
Box::new(m20260719_000001_add_upload_provider_session::Migration),
Box::new(m20260723_000001_require_upload_session_kind::Migration),
]
}
}
Expand Down
Loading