feat: add agent-friendly ufile CLI

This commit is contained in:
loveuer
2026-09-21 00:45:35 -07:00
parent f0c29e4832
commit a58990ce66
17 changed files with 1684 additions and 3 deletions
+177
View File
@@ -0,0 +1,177 @@
package main
import (
"fmt"
"io"
"strings"
)
const rootHelp = `ufile-cli — 面向自动化与 Agent 的 ufile 命令行客户端
用法:
ufile-cli [全局选项] <命令> [命令选项]
Agent 快速开始:
printf '%s\n' "$UFILE_PASSWORD" | ufile-cli -H https://ufile.example.com auth login -U agent --password-stdin
ufile-cli upload ./result.zip -R 1d -D 5
ufile-cli download Ab3dE8xP -C 123456 -O ./result.zip
命令:
auth login 登录并安全保存 30 天 Cookie 会话
auth whoami 输出当前登录用户和权限
auth logout 注销当前会话并清除本地凭据
upload 上传文件;默认自动生成 6 位取件码
download 下载文件;直接下载,不执行额外的取件校验
version 输出版本、提交和构建时间
help [命令] 显示根帮助或指定命令的详细帮助
全局选项(必须写在命令之前):
-H, --host URL ufile 服务地址;--server 是等价别名
--config PATH 配置文件路径
--timeout TIME 单次请求超时,默认 10m;0 表示不限制
--help 显示帮助
-V, --version 输出版本信息
环境变量:
UFILE_HOST 服务地址;UFILE_SERVER 是兼容别名
UFILE_SESSION 临时覆盖配置文件中的会话 Cookie
UFILE_USERNAME auth login 的默认用户名
UFILE_PASSWORD auth login 的非交互密码;优先使用密钥管理器注入
UFILE_CONFIG 默认配置文件路径
输出契约:
成功结果以单个 JSON 对象写入 stdout。
错误以单个 JSON 对象写入 stderr,并返回非零退出码。
download -O - 是唯一例外:stdout 只包含原始文件字节,不输出 JSON。
Agent 应以退出码判断成功,不要解析本地化的 message 文案。
错误 JSON:
{"error":{"code":"permission_denied","message":"没有上传权限","httpStatus":403}}
退出码:
0 成功
1 未分类错误
2 参数或用法错误
3 未登录、会话过期或认证失败
4 权限不足、缺少取件码或业务限制
5 文件不存在、已失效或取件码错误
6 服务端输入校验失败
7 网络错误或服务端内部错误
8 本地配置或文件读写错误
安全约定:
upload 默认生成取件码;只有显式传入 --no-pickup-code 才公开文件。
错误或缺失的取件码累计 5 次会使服务端删除整个文件。
download 不会预先调用校验接口,避免一次操作产生多次校验请求。
密码不能作为命令行参数传入,避免出现在进程列表和 Agent 日志中。
pickupCode 和 downloadUrl 可能包含访问凭据,不应写入公开日志。
详细帮助:
ufile-cli help auth
ufile-cli help upload
ufile-cli help download
`
const authHelp = `ufile-cli auth — 管理 Cookie 会话
用法:
ufile-cli [全局选项] auth <login|whoami|logout> [选项]
登录:
ufile-cli -H https://ufile.example.com auth login -U USER
printf '%s\n' "$UFILE_PASSWORD" | ufile-cli -H https://ufile.example.com auth login -U USER --password-stdin
login 选项:
-U, --username USER 用户名;也可使用 UFILE_USERNAME
--password-stdin 从标准输入读取一行密码,适合 Agent 和 CI
--help 显示本帮助
密码来源优先级:
1. --password-stdin
2. UFILE_PASSWORD
3. 交互式终端中的隐藏输入
说明:
会话 Cookie 有效期由服务端决定,目前为 30 天。
密码永远不会写入配置文件。
首次登录后必须改密的账号,需要先在 Web 界面完成密码修改。
`
const uploadHelp = `ufile-cli upload — 上传文件并输出分享信息
用法:
ufile-cli [全局选项] upload <文件路径> [选项]
command | ufile-cli [全局选项] upload - -N <文件名> [选项]
示例:
ufile-cli upload ./report.zip -R 1d -D 5
ufile-cli upload ./public.txt -R 1h -D 1 --no-pickup-code
ufile-cli upload ./secret.pdf -P 123456
command | ufile-cli upload - -N result.txt -R 8h
选项:
-R, --retention VALUE 保留时间:1h、8h、1d、3d、forever;默认 1d
-D, --downloads VALUE 下载次数:1-99 或 unlimited;默认 5
-P, --pickup-code CODE 使用指定的 6 位数字取件码
--no-pickup-code 明确创建无需取件码的公开分享
-N, --name NAME 从标准输入上传时必需的文件名
--content-type TYPE 覆盖根据扩展名推断的 MIME 类型
--help 显示本帮助
安全与权限:
未指定 -P 或 --no-pickup-code 时,会使用密码学安全随机数自动生成取件码。
-P 与 --no-pickup-code 不能同时使用。
普通上传单文件最大 10 MiB,且最多保留 10 个有效文件。
forever、unlimited 和超过 10 MiB 的文件需要高级上传权限。
成功 JSON 字段:
hash、name、size、expiresAt、maxDownloads、pickupCode、downloadUrl、previewUrl
示例输出:
{"hash":"Ab3dE8xP","name":"report.zip","size":1024,"expiresAt":"2026-09-21T12:00:00Z","maxDownloads":5,"pickupCode":"123456","downloadUrl":"https://ufile.example.com/uf/Ab3dE8xP?code=123456","previewUrl":"https://ufile.example.com/uf/Ab3dE8xP?code=123456&preview=true"}
`
const downloadHelp = `ufile-cli download — 下载一个公开分享文件
用法:
ufile-cli [全局选项] download <8位文件码> -O <输出路径> [选项]
示例:
ufile-cli download Ab3dE8xP -C 123456 -O ./report.zip
ufile-cli download Ab3dE8xP -O ./public.txt
ufile-cli download Ab3dE8xP -C 123456 -O - > report.zip
选项:
-C, --code CODE 6 位数字取件码
-O, --output PATH 输出路径;使用 - 将原始文件字节写入 stdout
--force 覆盖已存在的目标文件
--help 显示本帮助
重要副作用:
每次成功下载都会消耗一次访问次数;次数耗尽后服务端删除文件。
缺失或错误的取件码也会累计失败;达到 5 次时服务端删除文件。
为减少风险,本命令直接下载,不会先请求 /api/pickup 校验。
本地文件先写入同目录临时文件,成功后再重命名。
输出:
普通路径成功时,stdout 输出包含 path、name、size、contentType 的 JSON。
-O - 成功时,stdout 仅包含文件字节;错误 JSON 仍写入 stderr。
`
func printHelp(out, errOut io.Writer, topic string) int {
switch strings.TrimSpace(topic) {
case "", "root":
_, _ = io.WriteString(out, rootHelp)
case "auth", "auth login", "auth whoami", "auth logout":
_, _ = io.WriteString(out, authHelp)
case "upload":
_, _ = io.WriteString(out, uploadHelp)
case "download":
_, _ = io.WriteString(out, downloadHelp)
case "version":
fmt.Fprintln(out, "用法:ufile-cli version\n\n以 JSON 输出 version、commit 和 date。")
default:
return writeCommandError(errOut, usageError(fmt.Sprintf("未知帮助主题 %q", topic)))
}
return exitOK
}