200 lines
5.5 KiB
Go
200 lines
5.5 KiB
Go
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]))
|
|
}
|