开发

文件与版本

空间、路径和节点;通过块网关上传和下载;版本历史、版本命名和比较。

查看 Markdown

空间和路径

Shell
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:

Shell
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. 拿到节点(新建,或者沿用同名的已有文件):

Shell
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。

Shell
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. 提交版本:

Shell
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。

下载

先要票据,再从网关读:

Shell
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 Response

预览文件(rendition)

服务器会给图片和视频生成 thumb(封面图),给视频生成 proxy:720p 的 H.264 流,不管原片是什么编码、多大,任何浏览器都能马上播放。要票据时加 "rendition": "thumb" 或 "proxy" 就能拿到。你自己的工具也可以用 PUT /v1/spaces/{sid}/versions/{vid}/renditions/{kind} 附上预览(需要 renditions:write)。

版本

Shell
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 和附加信息:

JSON
{
  "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"}]}'
  • 每个操作把 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 列出所有人的状态,实时连接也会推送。