本文记录 fastcdc feature 下 Media 协议的服务端契约。这不是标准 Git LFS 扩展,也不是标准 Git BLAKE3 互通。media_oid、chunk hash 与 fallback OID 属于 LFS SHA-256 digest domain,不受仓库 monorepo.object_format / Git HashKind 影响。
- 算法:
fastcdc-v2020-32k(fastcdc = "=3.2.1",v2020,Normalization::Level1,seed=0,min/avg/max = 32/64/256 KiB)。旧fastcdc-v1/ 对象前缀v1/命名空间保留不读写、不自动删除。 - Manifest:
version=1,hash_algorithm=sha256,chunkcompression固定none且encoded_length == length。 - 块限制:非尾块
32768..=262144;尾块1..=262144;空文件零块。不设总 chunk 数或全文件字节产品上限。 - Canonical ID:SHA-256(小写 hex)over JSON of
(version, algorithm, hash_algorithm, media_oid, media_size, chunks)。created_by与fallback_oid不进入 identity;分页边界不参与身份(P-01)。 - 包装上限:Media metadata 单页/摘要/状态请求响应含包装 ≤ 1 MiB(
MAX_ENVELOPE_SIZE);单页最多 4096 条且紧凑 entries JSON ≤ 960 KiB(P-01a);chunk payload ≤ 256 KiB;created_by≤ 4096 bytes。完整 LFS 流不受页限额约束。 fallback_oid若存在必须等于media_oid。- HTTP 形状(路径以 Libra client 为准):
- capabilities:
GET <repo>.git/info/lfs/libra/media/v1/capabilities - prepare:
POST …/manifests→{manifest_id, missing_chunks}(唯一 hash 差集;exists 并发 ≤16) - page:
PUT …/manifests/{id}/pages/{page_no}(幂等;内容冲突 409) - seal:
POST …/manifests/{id}/seal→{manifest_id, seal_generation, page_count} - missing:
GET …/manifests/{id}/missing?cursor=→{hashes, next_cursor} - upload:
PUT …/manifests/{id}/chunks/{hash}(application/octet-stream) - finalize:
POST …/manifests/{id}/finalize→ 202 任务(MF-03);GET …/tasks/{task_id}轮询 - get:
GET …/manifests/by-media/{oid}→{manifest_id, manifest} - chunk download:
GET …/manifests/by-media/{oid}/chunks/{hash} - fixed-id summary:
GET …/finalized/{manifest_id}→ManifestSummary - fixed-id pages:
GET …/finalized/{manifest_id}/pages - fixed-id chunk:
GET …/finalized/{manifest_id}/chunks/{hash}
- capabilities:
Capabilities::v1() 同时保留旧字段名并输出共享表字段:
| 字段 | 值 |
|---|---|
| version | "1" |
| batch_exists / supports_batch_exists | true |
| range_read / supports_range_read | false |
| standard_lfs_fallback / supports_standard_lfs_fallback | true |
| supports_manifest_id_read | true |
| manifest_paging | "v1" |
| max_page_entries | 4096 |
| max_page_bytes / max_manifest_size | 1048576 |
| chunk_algorithms | ["fastcdc-v2020-32k"] |
| max_chunk_size | 262144 |
- Namespace 字符串
media只追加,不改 git/lfs/log/artifact/attachment/oci。 - Scope = 服务端 actor + canonical 绝对仓库路径;digest = SHA-256(
actor || 0x00 || repo)。 Actor 取AccessTokenUser的website_user_id(非空)否则username。请求体 actor/repository/created_by不得覆盖。 - Object key:
fastcdc-v2020-32k/{scope_digest}/{pending|chunk|manifest|finalized|page}/{id},存储路径media/+ key(Media 不走 3-level sharding)。旧v1/对象隔离保留。finalized/{manifest_id}:不可变身份(完整ManifestResponse)manifest/{media_oid}:可替换 by-media 发现page/{manifest_id}-p{n}:不可变页 blob
- 对外错误不泄漏 digest、object key 或认证信息。
- 激活顺序: 先发布本仓 MF-06,再 Libra FL-06,再 MF-05,最后 FL-05。服务端发布只依赖本仓测试与共享静态向量,不等待未来客户端。
- 命名空间切换: 新写入一律落在
media/fastcdc-v2020-32k/…。既有media/v1/…对象不读写、不自动删除;标准 LFSlfs/{oid}与lfs_objects行保留。同 oid 可在新空间重新 finalize,不受旧v1finalized 阻止。 - 补偿恢复(活动数据不可仅 revert 代码): 停写新 Media → 回切到匹配的一对旧二进制与旧
v1空间语义 → 继续服务保留的v1对象与标准 LFS。若活动数据已写入新空间,不得只把代码回退到旧前缀而指望读到新对象。 - 清理: 旧
v1空间与布局 GC 属DEFER-MF-03,须另行显式运维,不在本协议激活范围内。 - 发布声明边界: 无真 interop(MF-05 / Libra FL 未完成)时,只可声明本仓服务端能力已发布;双仓收口保持 blocked。
- 演练证据:
ceres::lfs::media::tests::c08_upgrade_retains_legacy_v1_and_standard_lfs(featurefastcdc)种植v1/与标准 LFS 对象后做新命名空间prepare,断言旧对象字节不变且新 pending 落在fastcdc-v2020-32k/。
服务库三表(迁移 m20260925_000100_media_paging):
media_session:(scope_digest, manifest_id);pending|sealed|finalized;seal_generation绑定缺块 cursor。media_entry:派生 chunk 索引;跨页 hash→length 冲突拒绝。media_task:异步 finalize lease(MF-03/07)。
prepare 在对象存储写入 pending 的同时 upsert_pending_session;put_page / seal / missing 走 MediaPagingStorage + page blob。
GET …/finalized/{manifest_id}→ManifestSummary(不可变布局;与 by-media 当前发现无关)。GET …/finalized/{manifest_id}/pages:顺序分页(cursor)或覆盖查询(offset+length启动,后续 cursor 绑定该范围)。响应含page_no、绝对offset_start/offset_end、entries 与next_cursor;单页包装 ≤1 MiB。GET …/finalized/{manifest_id}/chunks/{hash}:仅当 hash 属于该 id 的 finalized 布局;跨 actor/repo/非成员统一 404;存储块必须重新校验 SHA-256,损坏返回 409,不得返回未校验 payload。- 成员/页索引缓存(
membership_cache)按(scope_digest, manifest_id)计费,总计 ≤128 MiB(含索引开销);满后 LRU 驱逐,不拒绝大文件。缓存不跨 scope;pending TTL 在缓存命中时仍逐请求检查(R-MF-04)。
prepare只接受已validate的 manifest(≤1 MiB 包装),并强制fallback_oid = media_oid;按 P-01a 计算page_count。- pending 对象:
media/fastcdc-v2020-32k/{scope}/pending/{manifest_id},逻辑 TTL 24 小时。过期 session 不能继续 upload/get。 missing_chunks按 scoped chunk key 检查存在性(并发 ≤16);重复chunk_hash在响应中去重(保序)。- chunk 上传必须命中 pending 声明的 hash/length,并校验实际 SHA-256(≤ 256 KiB)。同 scope 同 hash 的正确对象可幂等复用;已存错误内容返回 Conflict。
- 领域错误:
Invalid/NotFound/Conflict/Storage/Io/Json。存储错误对外固定为media object store error。
- 同时最多 2 个 finalize(semaphore);活跃排队上限 128(超限 429 +
Retry-After)。 POST …/manifests/{id}/finalize仅接受 sealed 会话,返回 202{task_id,manifest_id,state,status_url};status_url为同前缀libra/media/v1/tasks/{task_id}(客户端不得跨 origin 跟随或转发 token)。GET …/tasks/{task_id}每请求重新做 scope 认证,返回state/bytes_verified/pages_verified/retryable/error_code;complete另含oid/size。- 验证:按 sealed 页序遍历
media_entry,逐块 scoped 读并校验 length/hash;连续覆盖media_size;整对象 SHA-256 须等于media_oid。不要求新鲜 FastCDC 冷切边界相等。 - 磁盘写与哈希在
spawn_blocking;单次 I/O ≤120s、无进展 ≤10min、lease 60s(约每 20s 续租);持续进展不受整文件墙钟限制。 - 缺失或损坏 chunk / gap / overlap / overflow:不写 LFS namespace、不写
lfs_objects、不发布 finalized;临时文件与 lease 在成功/失败/取消路径均释放。 - 通过后的发布顺序(C-04 / ADR-MF-05):
- 完整 LFS fallback
lfs/{oid}(put_stream_bounded)+lfs_objects同 oid 幂等(大小一致才接受); - 不可变身份记录
media/…/finalized/{manifest_id}(put_metadata_atomic,≤1 MiB 完整对象 PUT); - 持久
media_session.state=finalized; - 可替换发现
media/…/manifest/{media_oid}(by-media;任一完整已发布布局可成为当前值,无需胜者 CAS); - 仅在本调用新写入不可变记录且已可读后发
lfs.media.finalized。
- 完整 LFS fallback
- 同 oid、不同合法布局(不同
manifest_id)可并发成功;同 id 重试返回等价 canonical 身份(created_by不参与比较)。不得暴露半 JSON / 错 id。 GET …/manifests/by-media/{oid}只返回会话已finalized的完整ManifestResponse。固定 id HTTP 路由见上文「固定 id 读取」;immutable 记录仍可通过存储直读finalized/{manifest_id}。- 不支持原子完整对象 PUT 的后端必须对
put_metadata_atomic返回明确错误,禁止退化为边写边可见。
- 既有 finalize 路径在本次实际新写入 immutable finalized 记录成功且可读后发一次
lfs.media.finalized(契约见storage-events.md):scope 只填服务端MediaScope的 canonicalrepo_path,data 为oid,size,manifest_id,transfer="fastcdc"。 - prepare / chunk / fallback 中间步骤不发;同 id 重试(immutable 已存在)不发。
- 认证不改:Media 路由仍全部要求
AccessTokenUser。
- Feature-on 时挂在当前 LFS mount 下:逻辑前缀
libra/media/v1。OpenAPI 登记为/api/v1/lfs/libra/media/v1/...。 - 每一条 Media 路由(含 capabilities)都要求
AccessTokenUser(Bearer)。 - Body 上限:JSON/metadata 路由
DefaultBodyLimit1 MiB;chunk PUT 路由 256 KiB。另有Content-Length中间件在读 body 前返回 413。 - 错误:Invalid/Json → 400,NotFound → 404,Conflict → 409,Storage/Io → 500 且 body 固定
media object store error。
固定 fixture:src/ceres/lfs/media/fixtures/(合法 valid_v1.json / 空文件 empty.json / 非法 version 与 fallback)。valid_v1.json 字节 SHA-256:09ae74a7f69da0bbd2b3df12d8f4bb85413b8ba34ec5de123f68141219ac3b96。
默认 cargo test --all 不要求 sibling Libra checkout。真实 client/server 证据只在显式 ignored target 下执行(见既有 integration_fastcdc_libra 说明)。feature-off 时 Media 能力探测为 404,Libra 选择标准 LFS fallback。
MF-05 pin: Libra v0.30.4 / 8c870c4d41eff9bff33dd3adf4b34189fc9ca1fe(FL-04 range export)。
Harness 设置 LIBRA_DIR 为该 tip 的干净 checkout,驱动 Libra
monoengine_fastcdc_http_interop(MONOENGINE_FASTCDC_READY_FILE),覆盖
prior→上传→固定 id 范围导出;feature-off 伴生用例验证标准 LFS 回退。
不得缺配置 SKIP-green。C-06 / P-05 大对象效果数字与资源预算见本卡发布证据与
Libra FL-05 收口。
MF-06(v0.40.10 家族发布)状态: 服务端家族(配方、分页协议、覆盖验证、多布局原子发布、固定 id 读取)已按本契约上线;MF-05 真 interop 在独立 patch 收口。