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 }