11 KiB
drive +push
前置条件: 先阅读
../lark-shared/SKILL.md了解认证、全局参数和安全规则。
把本地目录单向、文件级镜像到飞书云空间的某个文件夹(本地 → Drive)。命令递归列出 --folder-token 下的远端清单,遍历 --local-dir 的所有常规文件,按相对路径在 Drive 上新建、覆盖或跳过;可选地(--delete-remote --yes)删除云端"本地没有"的 type=file。
"文件级镜像"≠"目录镜像"。 命令只在文件维度收敛差异:本地多了文件就上传,本地少了文件且开了
--delete-remote --yes就删远端文件。远端只有的空目录、本地已删除的目录都不会被收敛,云端目录树的多余结构不会被清理。如果需要"目录也要保持完全一致",得自行先+status找差异、再手动处理多余目录。
输出按"动作"分类:
| 字段 | 含义 |
|---|---|
summary.uploaded |
成功新建或覆盖的文件数 |
summary.skipped |
按 --if-exists=skip 跳过的文件数 |
summary.failed |
上传 / 覆盖 / 建目录 / 删除失败的条目数;只要不为 0,命令就以非零状态退出(结构化 items[] 仍在 stdout 上) |
summary.deleted_remote |
启用 --delete-remote --yes 时删除的云端文件数 |
items[] |
每个条目的明细(rel_path / file_token / action / 覆盖时的 version / size_bytes / 失败时的 error) |
items[].action 取值:uploaded / overwritten / skipped / folder_created / deleted_remote / failed / delete_failed。
本地目录(包括空目录)会被镜像到 Drive;新建的子目录会以
action: "folder_created"出现在items[]里,但不计入summary.uploaded(该字段只数文件)。已存在的远端目录复用其 token,不会重复create_folder,也不会出现在items[]里。
命令
# 基础用法 —— 把本地 ./repo 增量推送到云端 fldcXXX
# 默认 --if-exists=skip:已经存在的远端文件保持不动,只新增、不覆盖。
lark-cli drive +push --local-dir ./repo --folder-token fldcnxxxxxxxxx
# 显式覆盖远端同名文件(依赖 upload_all 的灰度协议字段,详见下文"覆盖语义")
lark-cli drive +push --local-dir ./repo --folder-token fldcnxxxxxxxxx \
--if-exists overwrite
# 文件级镜像同步:上传 / 覆盖 + 删除本地不存在的远端文件
# (--delete-remote 必须搭配 --yes,否则会被 Validate 直接拒绝;
# 且 Validate 阶段会动态检查 space:document:delete scope,缺权限会立刻失败,
# 不会出现"上传成功了但是后面删除阶段挂了"的半同步状态)
lark-cli drive +push --local-dir ./repo --folder-token fldcnxxxxxxxxx \
--if-exists overwrite --delete-remote --yes
参数
| 标志 | 必填 | 类型 | 说明 |
|---|---|---|---|
--local-dir |
是 | path | 本地根目录(必须是 cwd 的相对路径;绝对路径或逃出 cwd 的相对路径会被 CLI 直接拒绝) |
--folder-token |
是 | string | 目标 Drive 文件夹 token |
--if-exists |
否 | enum | 远端文件已存在时的策略:skip(默认,安全)/ overwrite(依赖灰度后端协议,详见"覆盖语义") |
--delete-remote |
否 | bool | 删除云端本地不存在的文件(文件级镜像;不会清理远端只有的目录);必须配合 --yes,且 Validate 阶段会动态检查 space:document:delete scope |
--yes |
否 | bool | 确认 --delete-remote;不传时该破坏性操作在 Validate 阶段被拒绝 |
上传与目录复刻范围
- 只上传 / 覆盖 / 删除 Drive
type=file。在线文档(docx/sheet/bitable/mindnote/slides)和快捷方式(shortcut)即使在同一 rel_path 下出现,也不会被覆盖或删除 —— 它们没有等价的本地二进制。 - 本地目录结构整体被镜像:所有子目录(含空目录)会按需在 Drive 上
create_folder;同名远端目录复用其 token,不重建。空目录不计入summary.uploaded,但会在items[]里以folder_created形式留痕。 - 已存在的远端文件按
--if-exists决定overwrite还是skip,没有第三种选择 —— 想做keep-both这类的请自行改名再 push。
覆盖语义
--if-exists=overwrite 走 POST /open-apis/drive/v1/files/upload_all,并在 form 中带上现有文件的 file_token,由后端原地更新内容并返回新版本号。items[].version 字段会回填该版本号。
为什么默认是
skip而不是overwrite:upload_all接受file_token字段、并在响应里返回version是设计文档(Drive 同步盘)规定的协议;此后端尚在灰度发布。在还未开通该字段的 tenant 上,--if-exists=overwrite会因"无 version 返回"而把对应文件标成failed,整次+push也会因此非零退出。所以默认值故意定为skip:第一次往一个已经有内容的目录里 push,不会因为协议没到位就把整次运行打挂;要真的覆盖远端,必须显式带--if-exists overwrite。新建上传不依赖该字段,不受影响。
大文件(>20MB)会自动切到三段式 upload_prepare / upload_part / upload_finish;该路径下 version 暂未在响应中返回,覆盖结果中 items[].version 会留空,但 file_token 与 action: overwritten 仍会正确产出。
--delete-remote 的安全行为
--delete-remote 是命令里唯一的破坏性 flag,会按"远端有但本地没有"逐个 DELETE /open-apis/drive/v1/files/<token>?type=file 清理云端副本。设计上把它跟 --yes 强绑定:
--delete-remote(无--yes)→ Validate 直接报错:--delete-remote requires --yes,不会发起任何列表 / 上传 / 删除请求。--delete-remote --yes→ Validate 阶段还会动态做一次space:document:delete的 scope 预检:缺这条 scope 时整次运行立刻失败、不发任何上传请求,避免出现"上传都成功了,但删除阶段才报 missing_scope"的半同步状态。--delete-remote --yes(且 scope 已授权)→ 正常执行:先把本地文件 push 上去,再扫一遍远端type=file列表,把不在本地清单里的逐个删除。任何上传 / 覆盖 / 建目录失败时,整段--delete-remote阶段会被跳过(stderr 上有提示),命令以非零状态退出,远端不会被破坏。- 不传
--delete-remote→summary.deleted_remote永远是 0;命令对远端"多余"文件视而不见。 - 在线文档(docx / sheet / bitable / ...)和快捷方式即使本地完全没有同名文件,也不会进入删除候选,因为它们从来不进
summary.uploaded的对齐域。 - 远端只有的空目录、本地已删除的目录也不会被清理 —— 这是"文件级镜像"的语义边界,命令不会对目录结构做主动收敛。
第 6 章里把 +push --delete-remote 标了 high-risk-write,CLI 这边的实现等价于"未传 --yes 时拒绝执行 + 动态 scope 预检",符合该约束的精神。
输出 schema
{
"summary": {
"uploaded": 0,
"skipped": 0,
"failed": 0,
"deleted_remote": 0
},
"items": [
{"rel_path": "...", "file_token": "...", "action": "folder_created"},
{"rel_path": "...", "file_token": "...", "action": "uploaded", "size_bytes": 0},
{"rel_path": "...", "file_token": "...", "action": "overwritten", "version": "...", "size_bytes": 0},
{"rel_path": "...", "file_token": "...", "action": "skipped", "size_bytes": 0},
{"rel_path": "...", "action": "failed", "size_bytes": 0, "error": "..."},
{"rel_path": "...", "file_token": "...", "action": "deleted_remote"},
{"rel_path": "...", "file_token": "...", "action": "delete_failed", "error": "..."}
]
}
rel_path 始终用 / 作为分隔符(跨平台一致)。
性能注意
- 上传流量 ≈ 本地待上传文件的总字节数。push 是全量上传 —— 跟
+status不一样,不会按 hash 跳过"内容相同"的文件(status 是按 hash 比较,push 是按--if-exists),所以一次跑可能很重。 - 想避免重跑全量,可以先
+status找出new_local和modified,再只对这些文件单独上传 / 覆盖。 - 大文件会用三段式分片上传(不会把整个 body 读进内存),但本地磁盘和上行带宽需要够。
所需 scope
| 操作 | scope | 是否在命令上预声明 |
|---|---|---|
| 列出文件夹 / 子目录 | drive:drive.metadata:readonly |
✅ 预声明 |
| 上传 / 覆盖文件 | drive:file:upload |
✅ 预声明 |
新建子目录(create_folder) |
space:folder:create |
✅ 预声明 |
删除文件(仅 --delete-remote --yes) |
space:document:delete |
⚙️ 不在命令默认 Scopes 里,但在 --delete-remote --yes 时由 Validate 动态预检 |
drive:drive 在部分企业被策略禁用,所以 +push 故意只声明上面这几条细粒度 scope。
关于
space:document:delete: 框架的 scope 预检(runner.go: checkShortcutScopes)会在Validate和--dry-run之前就把命令上声明的 scope 全检查一遍;如果把删除 scope 也预声明,普通上传或 dry-run 都会因为没授权删除权限而被拦下来。所以这一项不放在命令的默认 Scopes 里,而是在 Validate 中条件触发:只有--delete-remote --yes同时打开时才会调用runtime.EnsureScopes([]string{"space:document:delete"})做一次动态前置校验。这样既保留了"普通上传不需要删除权限"的便利,又能在真要做镜像删除前把 scope 缺失暴露出来,避免出现"上传成功 → 删除阶段才挂"的半同步状态。想一次性把权限补齐:
lark-cli auth login --scope "drive:drive.metadata:readonly drive:file:upload space:folder:create space:document:delete"。
范围限制
--local-dir 只接受 cwd 内的相对路径。CLI 会先 EvalSymlinks 整条路径,再判断它是否仍落在 cwd 内 —— 指向 cwd 外的符号链接也会被拒,"在 cwd 内放一条软链指向外面" 这条捷径走不通,会直接撞上 unsafe file path。
如果用户想 push cwd 之外的目录,不要 agent 自己 cd 绕过。可以选:让用户在外部把 agent 工作目录切换到目标的祖先后重启会话;或者把目标整体物理移动 / 拷贝到 cwd 内(不是软链);或者直接放弃这次同步,改用别的方式。
参考
- lark-drive —— 云空间全部命令
- lark-shared —— 认证和全局参数
- lark-drive-status —— 上传前先看差异(避免全量回写)
- lark-drive-pull —— Drive → 本地的对称命令
- lark-drive-upload —— 单文件按需上传