feat: add agent-friendly ufile CLI
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "dev"
|
||||
commit = "unknown"
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
const (
|
||||
exitOK = 0
|
||||
exitGeneral = 1
|
||||
exitUsage = 2
|
||||
exitAuth = 3
|
||||
exitPermission = 4
|
||||
exitUnavailable = 5
|
||||
exitValidation = 6
|
||||
exitNetwork = 7
|
||||
exitLocalIO = 8
|
||||
)
|
||||
|
||||
type streams struct {
|
||||
in io.Reader
|
||||
out io.Writer
|
||||
errOut io.Writer
|
||||
stdin *os.File
|
||||
}
|
||||
|
||||
type globalOptions struct {
|
||||
host string
|
||||
configPath string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
os.Exit(run(ctx, os.Args[1:], streams{in: os.Stdin, out: os.Stdout, errOut: os.Stderr, stdin: os.Stdin}))
|
||||
}
|
||||
|
||||
func run(ctx context.Context, args []string, ioStreams streams) int {
|
||||
options, rest, help, err := parseGlobalOptions(args)
|
||||
if err != nil {
|
||||
return writeCommandError(ioStreams.errOut, usageError(err.Error()))
|
||||
}
|
||||
if help || len(rest) == 0 {
|
||||
_, _ = io.WriteString(ioStreams.out, rootHelp)
|
||||
return exitOK
|
||||
}
|
||||
if rest[0] == "help" {
|
||||
topic := ""
|
||||
if len(rest) > 1 {
|
||||
topic = strings.Join(rest[1:], " ")
|
||||
}
|
||||
return printHelp(ioStreams.out, ioStreams.errOut, topic)
|
||||
}
|
||||
if rest[0] == "version" {
|
||||
return writeJSON(ioStreams.out, map[string]string{"version": version, "commit": commit, "date": date})
|
||||
}
|
||||
if helpTopic, ok := requestedCommandHelp(rest); ok {
|
||||
return printHelp(ioStreams.out, ioStreams.errOut, helpTopic)
|
||||
}
|
||||
if err := validateCommand(rest); err != nil {
|
||||
return writeCommandError(ioStreams.errOut, err)
|
||||
}
|
||||
|
||||
config, err := loadConfig(options.configPath)
|
||||
if err != nil {
|
||||
return writeCommandError(ioStreams.errOut, localError("读取配置失败", err))
|
||||
}
|
||||
host := firstNonEmpty(options.host, os.Getenv("UFILE_HOST"), os.Getenv("UFILE_SERVER"), config.Host)
|
||||
if host == "" {
|
||||
return writeCommandError(ioStreams.errOut, usageError("缺少服务地址;请使用 -H/--host,或设置 UFILE_HOST"))
|
||||
}
|
||||
host, err = normalizeHost(host)
|
||||
if err != nil {
|
||||
return writeCommandError(ioStreams.errOut, usageError(err.Error()))
|
||||
}
|
||||
session := firstNonEmpty(os.Getenv("UFILE_SESSION"), config.Session)
|
||||
client := newAPIClient(host, session, options.timeout)
|
||||
|
||||
var commandErr *commandError
|
||||
switch rest[0] {
|
||||
case "auth":
|
||||
commandErr = runAuth(ctx, rest[1:], options.configPath, config, client, ioStreams)
|
||||
case "upload":
|
||||
commandErr = runUpload(ctx, rest[1:], client, ioStreams)
|
||||
case "download":
|
||||
commandErr = runDownload(ctx, rest[1:], client, ioStreams)
|
||||
default:
|
||||
commandErr = usageError(fmt.Sprintf("未知命令 %q;运行 ufile-cli help 查看可用命令", rest[0]))
|
||||
}
|
||||
if commandErr != nil {
|
||||
return writeCommandError(ioStreams.errOut, commandErr)
|
||||
}
|
||||
return exitOK
|
||||
}
|
||||
|
||||
func parseGlobalOptions(args []string) (globalOptions, []string, bool, error) {
|
||||
var options globalOptions
|
||||
var help, showVersion bool
|
||||
fs := flag.NewFlagSet("ufile-cli", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
fs.StringVar(&options.host, "H", "", "ufile 服务地址")
|
||||
fs.StringVar(&options.host, "host", "", "ufile 服务地址")
|
||||
fs.StringVar(&options.host, "server", "", "ufile 服务地址(--host 的别名)")
|
||||
fs.StringVar(&options.configPath, "config", defaultConfigPath(), "配置文件路径")
|
||||
fs.DurationVar(&options.timeout, "timeout", 10*time.Minute, "单次请求超时")
|
||||
fs.BoolVar(&help, "help", false, "显示帮助")
|
||||
fs.BoolVar(&showVersion, "V", false, "显示版本")
|
||||
fs.BoolVar(&showVersion, "version", false, "显示版本")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return options, nil, false, err
|
||||
}
|
||||
if options.timeout < 0 {
|
||||
return options, nil, false, fmt.Errorf("--timeout 不能为负数")
|
||||
}
|
||||
if showVersion {
|
||||
return options, []string{"version"}, false, nil
|
||||
}
|
||||
return options, fs.Args(), help, nil
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func interspersedArgs(args []string, valueFlags map[string]bool) []string {
|
||||
options := make([]string, 0, len(args))
|
||||
positionals := make([]string, 0, len(args))
|
||||
for index := 0; index < len(args); index++ {
|
||||
argument := args[index]
|
||||
if argument == "--" {
|
||||
positionals = append(positionals, args[index+1:]...)
|
||||
break
|
||||
}
|
||||
if argument == "-" || !strings.HasPrefix(argument, "-") {
|
||||
positionals = append(positionals, argument)
|
||||
continue
|
||||
}
|
||||
options = append(options, argument)
|
||||
name := strings.TrimLeft(strings.SplitN(argument, "=", 2)[0], "-")
|
||||
if valueFlags[name] && !strings.Contains(argument, "=") && index+1 < len(args) {
|
||||
index++
|
||||
options = append(options, args[index])
|
||||
}
|
||||
}
|
||||
return append(options, positionals...)
|
||||
}
|
||||
|
||||
func requestedCommandHelp(args []string) (string, bool) {
|
||||
if args[0] == "auth" && len(args) > 1 && args[1] == "help" {
|
||||
return "auth", true
|
||||
}
|
||||
for _, argument := range args[1:] {
|
||||
if argument == "--help" {
|
||||
topic := args[0]
|
||||
if topic == "auth" && len(args) > 1 && !strings.HasPrefix(args[1], "-") {
|
||||
topic += " " + args[1]
|
||||
}
|
||||
return topic, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func validateCommand(args []string) *commandError {
|
||||
switch args[0] {
|
||||
case "upload", "download":
|
||||
return nil
|
||||
case "auth":
|
||||
if len(args) < 2 {
|
||||
return usageError("auth 需要子命令 login、whoami 或 logout;运行 ufile-cli help auth")
|
||||
}
|
||||
switch args[1] {
|
||||
case "login", "whoami", "logout", "help":
|
||||
return nil
|
||||
default:
|
||||
return usageError(fmt.Sprintf("未知 auth 子命令 %q;运行 ufile-cli help auth", args[1]))
|
||||
}
|
||||
}
|
||||
return usageError(fmt.Sprintf("未知命令 %q;运行 ufile-cli help 查看可用命令", args[0]))
|
||||
}
|
||||
Reference in New Issue
Block a user