aboutsummaryrefslogtreecommitdiff
path: root/internal/cli/cmd_main.go
blob: 3d4c3dbf35af1ee6d32690b491f9d87eb378009b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package cli

import (
	"context"
	"fmt"
	"os"
	"strings"

	"github.com/spf13/pflag"

	"tsne.dev/hopper/internal/log"
)

var _ command = (*mainCommand)(nil)

type mainCommand struct {
	flags *pflag.FlagSet

	zone          string
	endpoint      string
	accessKeyFile string
	accessKey     string
	verbose       bool
	quiet         bool
}

func newMainCommand() *mainCommand {
	cmd := &mainCommand{}
	cmd.flags = newFlagset("hopper")
	cmd.flags.SetInterspersed(false)
	cmd.flags.StringVar(&cmd.zone,
		"zone",
		lookupEnv("BUNNY_ZONE", ""),
		"Bunny storage zone name (required) (env: BUNNY_ZONE)",
	)
	cmd.flags.StringVar(&cmd.endpoint,
		"endpoint",
		lookupEnv("BUNNY_ENDPOINT", "storage.bunnycdn.com"),
		"Bunny storage endpoint hostname (env: BUNNY_ENDPOINT)",
	)
	cmd.flags.StringVar(&cmd.accessKeyFile,
		"access-key-file",
		lookupEnv("BUNNY_ACCESS_KEY_FILE", ""),
		"path to a file containing the storage access key (env: BUNNY_ACCESS_KEY_FILE)",
	)
	cmd.flags.BoolVar(
		&cmd.verbose,
		"verbose",
		false,
		"print more details",
	)
	cmd.flags.BoolVar(
		&cmd.quiet,
		"quiet",
		false,
		"print only the final summary and errors",
	)
	return cmd
}

func (cmd *mainCommand) Usage() string {
	return "Usage:" +
		"\n    hopper [global flags] ⟨command⟩ [command flags]" +
		"\n" +
		"\nCommands:" +
		"\n    push    upload a release to Bunny Storage" +
		"\n    prune   remove orphaned files from Bunny Storage" +
		"\n" +
		"\nGlobal flags:" +
		"\n" + cmd.flags.FlagUsages()
}

func (cmd *mainCommand) Run(ctx context.Context, args []string) error {
	parseFlags(cmd, cmd.flags, args)

	log.SetVerbose(cmd.verbose)
	log.SetQuiet(cmd.quiet)

	if cmd.accessKeyFile == "" {
		cmd.accessKey = os.Getenv("BUNNY_ACCESS_KEY")
	}

	switch {
	case cmd.zone == "":
		return usageError{"missing zone (provide --zone or BUNNY_ZONE)"}
	case cmd.endpoint == "":
		// --endpoint has a default, so an empty value here is always deliberate.
		return usageError{"the endpoint must not be empty"}
	case cmd.accessKey == "" && cmd.accessKeyFile == "":
		return usageError{"missing access key (provide --access-key-file, BUNNY_ACCESS_KEY_FILE, or BUNNY_ACCESS_KEY)"}
	}

	subargs := cmd.flags.Args()
	if len(subargs) == 0 || subargs[0] == "" {
		return usageError{"missing subcommand"}
	}

	var sub command
	switch subargs[0] {
	case "push":
		sub = newPushCommand(cmd)
	case "prune":
		sub = newPruneCommand(cmd)
	default:
		return usageError{fmt.Sprintf("unknown subcommand %q", subargs[0])}
	}

	return runCommand(ctx, sub, subargs[1:])
}

func (cmd *mainCommand) lookupAccessKey() error {
	if cmd.accessKey == "" {
		accessKey, err := os.ReadFile(cmd.accessKeyFile)
		if err != nil {
			return fmt.Errorf("read access key file: %w", err)
		}
		cmd.accessKey = strings.TrimSpace(string(accessKey))
	}
	if cmd.accessKey == "" {
		return usageError{"empty access key"}
	}
	return nil
}

func lookupEnv(env string, defaultValue string) string {
	if val := os.Getenv(env); val != "" {
		return val
	}
	return defaultValue
}