Files
warpbox/cmd/cmd_box.go
Daniel Legt 877ac90574 feat(cli): add comprehensive command suite
+ Adds new commands for managing boxes, environment variables, and running the server.
* The command structure is greatly expanded to improve user experience and coverage for core service functionalities.
2026-04-30 11:31:43 +03:00

369 lines
10 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/crypto/bcrypt"
"warpbox/lib/boxstore"
"warpbox/lib/helpers"
"warpbox/lib/models"
)
func newBoxCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "box",
Short: "Manage boxes",
Long: "Manage WarpBox upload boxes: list, view, inspect, delete, modify.",
}
cmd.AddCommand(newBoxListCommand())
cmd.AddCommand(newBoxViewCommand())
cmd.AddCommand(newBoxInspectCommand())
cmd.AddCommand(newBoxDeleteCommand())
cmd.AddCommand(newBoxChangeCommand())
cmd.AddCommand(newBoxGetCommand())
return cmd
}
func newBoxListCommand() *cobra.Command {
var format string
var uploadRoot string
cmd := &cobra.Command{
Use: "ls",
Aliases: []string{"list", "view"},
Short: "List all boxes",
RunE: func(cmd *cobra.Command, args []string) error {
if uploadRoot != "" {
boxstore.SetUploadRoot(uploadRoot)
}
summaries, err := boxstore.ListBoxSummaries()
if err != nil {
return fmt.Errorf("failed to list boxes: %w", err)
}
if len(summaries) == 0 {
fmt.Println("No boxes found.")
return nil
}
switch format {
case "json":
return formatBoxSummariesJSON(summaries)
case "table", "":
return formatBoxSummariesTable(summaries)
default:
return fmt.Errorf("unknown format: %s (use 'table' or 'json')", format)
}
},
}
cmd.Flags().StringVarP(&format, "format", "o", "table", "Output format: table, json")
cmd.Flags().StringVar(&uploadRoot, "upload-root", "", "Override upload root directory")
return cmd
}
func newBoxViewCommand() *cobra.Command {
var uploadRoot string
cmd := &cobra.Command{
Use: "view",
Short: "View box summary",
Long: "View a box summary showing files, size, expiry, etc.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if uploadRoot != "" {
boxstore.SetUploadRoot(uploadRoot)
}
boxID := args[0]
summary, err := boxstore.BoxSummary(boxID)
if err != nil {
return fmt.Errorf("failed to view box %s: %w", boxID, err)
}
printBoxSummary(&summary)
return nil
},
}
cmd.Flags().StringVar(&uploadRoot, "upload-root", "", "Override upload root directory")
return cmd
}
func newBoxInspectCommand() *cobra.Command {
var uploadRoot string
var full bool
cmd := &cobra.Command{
Use: "inspect",
Short: "Inspect box manifest (raw JSON)",
Long: "Print the full box manifest as JSON. Use --full for hidden fields.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if uploadRoot != "" {
boxstore.SetUploadRoot(uploadRoot)
}
boxID := args[0]
manifest, err := boxstore.ReadManifest(boxID)
if err != nil {
return fmt.Errorf("failed to read manifest for box %s: %w", boxID, err)
}
if !full {
sanitized := manifest
sanitized.PasswordHash = "[REDACTED]"
sanitized.PasswordSalt = "[REDACTED]"
sanitized.AuthToken = "[REDACTED]"
manifest = sanitized
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(manifest)
},
}
cmd.Flags().StringVar(&uploadRoot, "upload-root", "", "Override upload root directory")
cmd.Flags().BoolVar(&full, "full", false, "Show sensitive fields (password hash, auth token)")
return cmd
}
func newBoxDeleteCommand() *cobra.Command {
var uploadRoot string
var force bool
cmd := &cobra.Command{
Use: "rm",
Aliases: []string{"del", "delete"},
Short: "Delete a box",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if uploadRoot != "" {
boxstore.SetUploadRoot(uploadRoot)
}
boxID := args[0]
if !force {
fmt.Printf("This will permanently delete box %s and all its files.\n", boxID)
fmt.Print("Confirm (y/N): ")
var confirm string
if _, err := fmt.Scanln(&confirm); err != nil {
confirm = "n"
}
if strings.ToLower(strings.TrimSpace(confirm)) != "y" {
fmt.Println("Aborted.")
return nil
}
}
if err := boxstore.DeleteBox(boxID); err != nil {
return fmt.Errorf("failed to delete box %s: %w", boxID, err)
}
fmt.Printf("Box %s deleted.\n", boxID)
return nil
},
}
cmd.Flags().StringVar(&uploadRoot, "upload-root", "", "Override upload root directory")
cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation prompt")
return cmd
}
func newBoxChangeCommand() *cobra.Command {
var uploadRoot string
var retention int64
var retentionList bool
var password string
var zip bool
var oneTime bool
var renew bool
var renewSeconds int64
cmd := &cobra.Command{
Use: "change",
Aliases: []string{"update", "modify"},
Short: "Change box properties",
Long: "Change box properties: retention, password, zip, one-time download, renew expiry.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if uploadRoot != "" {
boxstore.SetUploadRoot(uploadRoot)
}
boxID := args[0]
if retentionList {
printRetentionOptions()
return nil
}
changes, err := gatherBoxChanges(cmd.Flags(), retention, password, zip, oneTime, renew, renewSeconds)
if err != nil {
return err
}
if len(changes) == 0 {
fmt.Println("No changes specified. Use --retention, --password, --zip, --one-time, --renew, or --retention-list.")
return nil
}
manifest, err := boxstore.ReadManifest(boxID)
if err != nil {
return fmt.Errorf("failed to read manifest for box %s: %w", boxID, err)
}
for _, apply := range changes {
if err := apply(&manifest); err != nil {
return err
}
}
if err := boxstore.WriteManifest(boxID, manifest); err != nil {
return fmt.Errorf("failed to save manifest for box %s: %w", boxID, err)
}
fmt.Printf("Box %s updated.\n", boxID)
return nil
},
}
cmd.Flags().StringVar(&uploadRoot, "upload-root", "", "Override upload root directory")
cmd.Flags().Int64Var(&retention, "retention", 0, "Set retention seconds (use --retention-list for valid values)")
cmd.Flags().BoolVar(&retentionList, "retention-list", false, "List available retention options")
cmd.Flags().StringVar(&password, "password", "", "Set a new password (empty string to remove)")
cmd.Flags().BoolVar(&zip, "zip", true, "Allow ZIP downloads (default true, --zip=false to disable)")
cmd.Flags().BoolVar(&oneTime, "one-time", false, "Enable one-time download mode")
cmd.Flags().BoolVar(&renew, "renew", false, "Renew box expiry (use --renew-seconds for duration)")
cmd.Flags().Int64Var(&renewSeconds, "renew-seconds", 0, "Seconds to extend expiry by (used with --renew)")
return cmd
}
type changeFunc func(*models.BoxManifest) error
func gatherBoxChanges(flags *pflag.FlagSet, retention int64, password string, zip bool, oneTime bool, renew bool, renewSeconds int64) ([]changeFunc, error) {
var changes []changeFunc
if flags.Changed("retention") {
if retention < 0 {
return nil, fmt.Errorf("retention cannot be negative")
}
changes = append(changes, func(m *models.BoxManifest) error {
if m.OneTimeDownload {
m.OneTimeDownload = false
}
m.RetentionSecs = retention
for _, opt := range boxstore.RetentionOptions() {
if opt.Seconds == retention {
m.RetentionKey = opt.Key
m.RetentionLabel = opt.Label
return nil
}
}
m.RetentionKey = "custom"
return nil
})
}
if flags.Changed("password") {
changes = append(changes, func(m *models.BoxManifest) error {
if password == "" {
m.PasswordHash = ""
m.PasswordHashAlg = ""
m.AuthToken = ""
return nil
}
token, err := helpers.RandomHexID(16)
if err != nil {
return fmt.Errorf("could not generate auth token")
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("could not hash password: %w", err)
}
m.PasswordHash = string(hash)
m.PasswordHashAlg = "bcrypt"
m.AuthToken = token
return nil
})
}
if flags.Changed("zip") {
changes = append(changes, func(m *models.BoxManifest) error {
if m.OneTimeDownload {
return nil
}
m.DisableZip = !zip
return nil
})
}
if flags.Changed("one-time") {
changes = append(changes, func(m *models.BoxManifest) error {
if oneTime {
m.OneTimeDownload = true
m.DisableZip = false
if boxstore.OneTimeDownloadExpiry() > 0 {
m.RetentionSecs = boxstore.OneTimeDownloadExpiry()
}
} else {
m.OneTimeDownload = false
}
return nil
})
}
if flags.Changed("renew") {
changes = append(changes, func(m *models.BoxManifest) error {
secs := renewSeconds
if secs <= 0 {
secs = m.RetentionSecs
}
return renewBoxExpiry(m, secs)
})
}
return changes, nil
}
func renewBoxExpiry(m *models.BoxManifest, seconds int64) error {
if seconds <= 0 || m.OneTimeDownload {
return nil
}
if m.ExpiresAt.IsZero() {
m.ExpiresAt = time.Now().UTC().Add(time.Duration(seconds) * time.Second)
return nil
}
m.ExpiresAt = m.ExpiresAt.Add(time.Duration(seconds) * time.Second)
return nil
}
func newBoxGetCommand() *cobra.Command {
var uploadRoot string
cmd := &cobra.Command{
Use: "get",
Short: "Get box URL and info",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if uploadRoot != "" {
boxstore.SetUploadRoot(uploadRoot)
}
boxID := args[0]
manifest, err := boxstore.ReadManifest(boxID)
if err != nil {
return fmt.Errorf("failed to read manifest for box %s: %w", boxID, err)
}
fmt.Printf("Box ID:\t%s\n", boxID)
fmt.Printf("URL:\t/box/%s\n", boxID)
if !manifest.CreatedAt.IsZero() {
fmt.Printf("Created:\t%s\n", manifest.CreatedAt.Format(time.RFC3339))
}
if !manifest.ExpiresAt.IsZero() {
fmt.Printf("Expires:\t%s\n", manifest.ExpiresAt.Format(time.RFC3339))
}
if boxstore.IsPasswordProtected(manifest) {
fmt.Println("Password:\tprotected")
}
if manifest.OneTimeDownload {
fmt.Println("Mode:\tone-time download")
}
return nil
},
}
cmd.Flags().StringVar(&uploadRoot, "upload-root", "", "Override upload root directory")
return cmd
}