开发
文件与版本
空间、路径和节点;通过块网关上传和下载;版本历史、版本命名和比较。
空间和路径
curl -H "Authorization: Bearer $SIDESPACE_TOKEN" https://side.example.com/v1/spaces每个空间有一个 root_node_id。它下面的每个文件和文件夹都是一个带 id(nod_…)的节点。可以按 id 操作,也可以按路径:
| 要做的事 | 调用 |
|---|---|
| 找到某个路径上的节点 | GET /v1/spaces/{sid}/paths/Ep01/Shots/A001_C003.mov |
| 列出文件夹(按名字排序、分页) | GET /v1/spaces/{sid}/nodes/{nid}/children?limit=200&cursor=… |
| 新建文件夹或空文件 | POST /v1/spaces/{sid}/nodes,带 parent_id、name、kind(dir 或 file) |
| 改名或移动 | PATCH /v1/spaces/{sid}/nodes/{nid},带 name 和/或 parent_id |
| 删除(进回收站,30 天) | DELETE /v1/spaces/{sid}/nodes/{nid} |
| 从回收站恢复 | POST /v1/spaces/{sid}/nodes/{nid}/restore |
同一文件夹里名字不能重复(不区分大小写)。新建时名字已被占用,默认的 if_exists: "rename" 会生成 A001_C003 (2).mov;if_exists: "reuse" 则返回已有的那个节点,这样两个程序保存同一个路径,结果是一个文件、两个版本。
PATCH 可以带 If-Match: <meta_rev> 请求头。如果在你读取之后有人改了名或移动了它,会返回 412,而不是覆盖别人的改动。
SDK 和 CLI 处处都接受路径,帮你解析成 id:
sidespace ls "Film A" Ep01/Shots
sidespace mv "Film A" Ep01/old.mov Ep01/Archive/old.mov
sidespace rm "Film A" Ep01/tmp文件内容怎么传
API 服务器从不经手文件内容。它发放访问块网关的短期票据;块网关把内容切成加密、去重的块(按内容切分,BLAKE3 哈希)存储,并负责流式读写。
- 同一批素材上传两次,或者只改了元数据的重新导出,只会传网关里没有的块。
- 读取支持 HTTP
Range,网关只读这个范围涉及的块。播放器可以直接从 200 GB 文件的中间开始播。 - 票据有效期 15 分钟;播放器用的票据加
"stream": true,有效 6 小时。
上传
SDK 和 CLI 一次调用就完成(side.upload(space, path, data))。直接用 HTTP 时分三步。
1. 拿到节点(新建,或者沿用同名的已有文件):
curl -X POST https://side.example.com/v1/spaces/$SPACE/nodes \
-H "Authorization: Bearer $SIDESPACE_TOKEN" -H "Content-Type: application/json" \
-d '{"parent_id": "'$FOLDER'", "name": "A001_C003.mov", "kind": "file", "if_exists": "reuse"}'2. 传内容。 开一个上传会话拿到上传票据,然后把整个文件流式发给网关。网关负责切块、存储,返回 manifest。
curl -X POST https://side.example.com/v1/spaces/$SPACE/upload-sessions \
-H "Authorization: Bearer $SIDESPACE_TOKEN" -H "Content-Type: application/json" -d '{"hashes": []}'
# {"missing": [], "ticket": "…", "blockgw_url": "https://blocks.side.example.com", "expires_at": "…"}
curl -X POST "https://blocks.side.example.com/stream-upload?t=$TICKET" --data-binary @A001_C003.mov
# {"algo": "fastcdc-v1", "size": 734003200, "chunks": [{"hash": "9f2c…", "offset": 0, "len": 1048576}, …]}在本地切块的客户端(Mac 应用和 CLI 就是这样)把块哈希放进 hashes,只用 PUT /chunks/{hash}?t=… 上传 missing 里的块,manifest 自己组装。
3. 提交版本:
curl -X POST https://side.example.com/v1/spaces/$SPACE/nodes/$NODE/versions \
-H "Authorization: Bearer $SIDESPACE_TOKEN" -H "Content-Type: application/json" \
-d '{"base_version_id": "ver_…", "manifest": {…}, "mime": "video/quicktime", "client_mtime": "2026-11-14T09:30:00Z"}'base_version_id 是你基于的那个版本(新文件填 null)。如果期间有人也保存了,你的版本照样成为当前版本(后保存的生效),返回里的 superseded 写明被替换的版本,它的作者会收到通知。什么都不会丢:两个版本都在历史里。缺块时返回 409 missing_chunks。
下载
先要票据,再从网关读:
curl -X POST https://side.example.com/v1/spaces/$SPACE/download-tickets \
-H "Authorization: Bearer $SIDESPACE_TOKEN" -H "Content-Type: application/json" \
-d '{"version_id": "ver_…"}'
# {"ticket": "…", "manifest": {…}, "blockgw_url": "https://blocks.side.example.com", "expires_at": "…"}
curl -H "Range: bytes=0-1048575" "https://blocks.side.example.com/files/ver_…?t=$TICKET" -o head.bin地址上加 download=<文件名> 会返回 Content-Disposition: attachment,方便做浏览器里的下载链接。网关对 /files/* 返回 CORS 头,所以网页可以直接播放或读取文件。
const bytes = await side.download("Film A", "/Ep01/notes.md");
const res = await side.open("Film A", "/Ep01/Shots/A001_C003.mov", { range: [0, 1_048_575] }); // 一个 fetch Responsedata = side.download("Film A", "/Ep01/notes.md")
with side.open("Film A", "/Ep01/Shots/A001_C003.mov", range=(0, 1048575)) as r:
head = r.read()预览文件(rendition)
服务器会给图片和视频生成 thumb(封面图),给视频生成 proxy:720p 的 H.264 流,不管原片是什么编码、多大,任何浏览器都能马上播放。要票据时加 "rendition": "thumb" 或 "proxy" 就能拿到。你自己的工具也可以用 PUT /v1/spaces/{sid}/versions/{vid}/renditions/{kind} 附上预览(需要 renditions:write)。
版本
sidespace versions "Film A" Ep01/cut.mp4
sidespace get "Film A" Ep01/cut.mp4 cut_v3.mp4 --version 3
sidespace label "Film A" Ep01/cut.mp4 3 "给客户的版本"
sidespace compare "Film A" Ep01/cut.mp4 "给客户的版本" # 和当前版本比较
sidespace restore "Film A" ver_01JD…| 要做的事 | 调用 |
|---|---|
| 历史,最新的在前 | GET /v1/spaces/{sid}/nodes/{nid}/versions |
| 让旧版本重新成为当前版本(复制一份,历史保留) | POST /v1/spaces/{sid}/versions/{vid}/restore |
给版本起名,或用 "" 清掉 | PUT /v1/spaces/{sid}/versions/{vid}/label |
| 两个版本之间改了什么 | GET /v1/spaces/{sid}/versions/{a}/compare/{b} |
同一个文件里,一个名字同时只属于一个版本(不区分大小写):把“给客户的版本”给了另一个版本,名字就会移过去。CLI 和 MCP 里凡是要填版本的地方,都可以写 id、序号(3 或 v3)或者版本名。
比较不下载任何东西,只看 manifest 和附加信息:
{
"same_content": false,
"size_delta": -1048576,
"bytes": { "shared": 690000000, "only_a": 44003200, "only_b": 42954624, "shared_ratio": 0.94 },
"media": [{ "field": "duration_s", "a": 92.4, "b": 88.0 }],
"sidecars": [{ "namespace": "ai.tags", "change": "changed", "fields": ["tags"] }],
"renditions": { "ver_A…": ["thumb", "proxy"], "ver_B…": ["thumb"] }
}shared_ratio 接近 1,说明只改了元数据或者剪短了一点;接近 0 说明是新素材。
想看整个文件夹在某一刻的样子,或者把它整体退回去,见看过去和整体回滚。
只改文件的一部分
不用重传整个文件,就能改中间的几个字节、插入、删掉一段、追加或截断,结果存成新版本。只有改动附近的数据块是新的,其余照旧复用,所以大文件也很快。
curl -X POST -H "Authorization: Bearer $SIDESPACE_TOKEN" -H 'If-Match: "ver_…"' -H "Content-Type: application/json" \
"$SIDESPACE_URL/v1/spaces/$SPACE/nodes/$NODE/edits" \
-d '{"ops": [{"at": 120, "delete": 4, "text": "night"}, {"at": 2048, "text": "\nINT. HARBOR"}]}'await side.edit("Film A", "/Ep01/script.md", [{ at: 120, delete: 4, text: "night" }], { ifVersionId: "ver_…" });
await side.appendText("Film A", "/Ep01/log.txt", "09:41 补拍完成\n");side.edit("Film A", "/Ep01/script.md", [{"at": 120, "delete": 4, "text": "night"}], if_version_id="ver_…")
side.append_text("Film A", "/Ep01/log.txt", "09:41 补拍完成\n")- 每个操作把
at开始的delete个字节换成text(或base64字节):delete为 0 是插入,不带数据是删除,at等于文件大小是追加。truncate最后把结果截到多少字节。 - 偏移量都按要改的那个版本、改动之前的位置算,范围不能重叠;一次最多 10,000 个操作、4 MiB 新数据。
If-Match(或请求体里的if_version_id)写你读到的版本。之后有人存过,就返回412,什么都不改,不会改到你没看过的内容上。不写的话改的是当前版本;改的过程中有人保存,也会失败。- 通过 MCP:
edit_text替换一段原文,append_text在末尾追加,见 MCP。
同步与在线状态
GET /v1/spaces/{sid}/snapshot分页列出所有现存节点,用于第一次同步。之后从 snapshot 的at_seq开始读变更日志。POST /v1/spaces/{sid}/presence,带{"node_id", "state": "viewing" | "editing" | "none"},告诉别人你正在看或改哪个文件。不续报的话 90 秒后失效。GET列出所有人的状态,实时连接也会推送。