aboutsummaryrefslogtreecommitdiff
path: root/internal/cli/cmd_main.go
diff options
context:
space:
mode:
authortsne <tsne.dev@outlook.com>2026-07-06 16:52:20 +0200
committertsne <tsne.dev@outlook.com>2026-07-06 21:37:42 +0200
commit84da43998f7cc42cc72652333ac6e3d293abb23e (patch)
tree6610f82df854ae86f7f34152bb05fc712ef6f314 /internal/cli/cmd_main.go
downloadhopper-main.tar.gz
initialHEADmain
Diffstat (limited to 'internal/cli/cmd_main.go')
-rw-r--r--internal/cli/cmd_main.go130
1 files changed, 130 insertions, 0 deletions
diff --git a/internal/cli/cmd_main.go b/internal/cli/cmd_main.go
new file mode 100644
index 0000000..3d4c3db
--- /dev/null
+++ b/internal/cli/cmd_main.go
@@ -0,0 +1,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
+}