diff options
Diffstat (limited to 'internal/cli/cmd_main.go')
| -rw-r--r-- | internal/cli/cmd_main.go | 130 |
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 +} |