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.
This commit is contained in:
368
cmd/cmd_box.go
Normal file
368
cmd/cmd_box.go
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
276
cmd/cmd_env.go
Normal file
276
cmd/cmd_env.go
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"warpbox/lib/config"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newEnvCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "env",
|
||||||
|
Short: "Explore environment variable options",
|
||||||
|
Long: "List and inspect WarpBox environment variables sourced from the codebase.",
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(newEnvListCommand())
|
||||||
|
cmd.AddCommand(newEnvDescribeCommand())
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEnvListCommand() *cobra.Command {
|
||||||
|
var format string
|
||||||
|
var includeHidden bool
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "ls",
|
||||||
|
Aliases: []string{"list"},
|
||||||
|
Short: "List all environment variables",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return formatEnvList(format, includeHidden)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(&format, "format", "o", "table", "Output format: table, json, env")
|
||||||
|
cmd.Flags().BoolVar(&includeHidden, "hidden", false, "Include non-editable and hard-limit settings")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEnvDescribeCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "describe",
|
||||||
|
Aliases: []string{"show", "info", "get"},
|
||||||
|
Short: "Describe an environment variable",
|
||||||
|
Long: "Show detailed info about a specific env var or setting key.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return describeEnvVar(args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
type envRow struct {
|
||||||
|
EnvName string
|
||||||
|
Key string
|
||||||
|
Label string
|
||||||
|
Type config.SettingType
|
||||||
|
Default string
|
||||||
|
Editable bool
|
||||||
|
HardLimit bool
|
||||||
|
Minimum int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type describeRow struct {
|
||||||
|
EnvName string
|
||||||
|
Key string
|
||||||
|
Label string
|
||||||
|
Type config.SettingType
|
||||||
|
Default string
|
||||||
|
Value string
|
||||||
|
Source string
|
||||||
|
Editable bool
|
||||||
|
HardLimit bool
|
||||||
|
Minimum int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatEnvList(format string, includeHidden bool) error {
|
||||||
|
allRows := buildAllEnvRows(includeHidden)
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case "json":
|
||||||
|
type envOut struct {
|
||||||
|
EnvName string `json:"env_name"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Default string `json:"default"`
|
||||||
|
Editable bool `json:"editable"`
|
||||||
|
HardLimit bool `json:"hard_limit"`
|
||||||
|
Minimum int64 `json:"minimum,omitempty"`
|
||||||
|
}
|
||||||
|
out := make([]envOut, len(allRows))
|
||||||
|
for i, r := range allRows {
|
||||||
|
out[i] = envOut{
|
||||||
|
EnvName: r.EnvName, Key: r.Key, Label: r.Label,
|
||||||
|
Type: string(r.Type), Default: r.Default, Editable: r.Editable,
|
||||||
|
HardLimit: r.HardLimit, Minimum: r.Minimum,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(out)
|
||||||
|
|
||||||
|
case "env":
|
||||||
|
for _, r := range allRows {
|
||||||
|
fmt.Printf("%s=\"%s\"\n", r.EnvName, r.Default)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case "table", "":
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
fmt.Fprintln(w, "ENV NAME\tKey\tLabel\tType\tDefault\tEditable")
|
||||||
|
for _, r := range allRows {
|
||||||
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%v\n",
|
||||||
|
r.EnvName, r.Key, r.Label, r.Type, r.Default, r.Editable)
|
||||||
|
}
|
||||||
|
return w.Flush()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown format: %s (use 'table', 'json', or 'env')", format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAllEnvRows(includeHidden bool) []envRow {
|
||||||
|
cfg, loadErr := config.Load()
|
||||||
|
|
||||||
|
var rows []envRow
|
||||||
|
|
||||||
|
for _, def := range config.Definitions {
|
||||||
|
if !includeHidden && (!def.Editable || def.HardLimit) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
row := envRow{
|
||||||
|
EnvName: def.EnvName,
|
||||||
|
Key: def.Key,
|
||||||
|
Label: def.Label,
|
||||||
|
Type: def.Type,
|
||||||
|
Editable: def.Editable,
|
||||||
|
HardLimit: def.HardLimit,
|
||||||
|
Minimum: def.Minimum,
|
||||||
|
}
|
||||||
|
if loadErr == nil {
|
||||||
|
row.Default = getEnvDefault(cfg, def)
|
||||||
|
}
|
||||||
|
rows = append(rows, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
extra := buildExtraEnvRows(includeHidden)
|
||||||
|
if loadErr == nil {
|
||||||
|
for i := range extra {
|
||||||
|
extra[i].Default = extra[i].Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows = append(rows, extra...)
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvDefault(cfg *config.Config, def config.SettingDefinition) string {
|
||||||
|
for _, row := range cfg.SettingRows() {
|
||||||
|
if row.Definition.Key == def.Key && row.Source == config.SourceDefault {
|
||||||
|
return row.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildExtraEnvRows(includeHidden bool) []envRow {
|
||||||
|
extra := []envRow{
|
||||||
|
{EnvName: "WARPBOX_ADMIN_ENABLED", Key: "admin_enabled", Label: "Admin interface mode", Type: config.SettingTypeText, Editable: false, Default: "auto"},
|
||||||
|
{EnvName: "WARPBOX_ADMIN_USERNAME", Key: "admin_username", Label: "Admin username", Type: config.SettingTypeText, Editable: false, Default: "admin"},
|
||||||
|
{EnvName: "WARPBOX_ADMIN_PASSWORD", Key: "admin_password", Label: "Admin password", Type: config.SettingTypeText, Editable: false, Default: "(none)"},
|
||||||
|
{EnvName: "WARPBOX_ADMIN_EMAIL", Key: "admin_email", Label: "Admin email", Type: config.SettingTypeText, Editable: false, Default: "(none)"},
|
||||||
|
{EnvName: "WARPBOX_ADMIN_COOKIE_SECURE", Key: "admin_cookie_secure", Label: "Admin cookie secure flag", Type: config.SettingTypeBool, Editable: false, Default: "false"},
|
||||||
|
{EnvName: "WARPBOX_ALLOW_ADMIN_SETTINGS_OVERRIDE", Key: "allow_admin_override", Label: "Allow admin UI to override settings", Type: config.SettingTypeBool, Editable: false, HardLimit: true, Default: "true"},
|
||||||
|
}
|
||||||
|
|
||||||
|
sizePairs := []struct {
|
||||||
|
bytesEnv string
|
||||||
|
mbEnv string
|
||||||
|
label string
|
||||||
|
}{
|
||||||
|
{"WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES", "WARPBOX_GLOBAL_MAX_FILE_SIZE_MB", "Global max file size"},
|
||||||
|
{"WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES", "WARPBOX_GLOBAL_MAX_BOX_SIZE_MB", "Global max box size"},
|
||||||
|
{"WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_BYTES", "WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_MB", "Default user max file size"},
|
||||||
|
{"WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_BYTES", "WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_MB", "Default user max box size"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pair := range sizePairs {
|
||||||
|
extra = append(extra, envRow{EnvName: pair.bytesEnv, Key: pair.bytesEnv, Label: pair.label + " (bytes)", Type: config.SettingTypeInt64, Editable: false, HardLimit: true, Minimum: 0, Default: "(use bytes or MB variant)"})
|
||||||
|
extra = append(extra, envRow{EnvName: pair.mbEnv, Key: pair.mbEnv, Label: pair.label + " (MB)", Type: config.SettingTypeInt64, Editable: false, HardLimit: true, Minimum: 0, Default: "(use bytes or MB variant)"})
|
||||||
|
}
|
||||||
|
|
||||||
|
return extra
|
||||||
|
}
|
||||||
|
|
||||||
|
func describeEnvVar(query string) error {
|
||||||
|
cfg, loadErr := config.Load()
|
||||||
|
|
||||||
|
for _, def := range config.Definitions {
|
||||||
|
if matchEnv(query, def.EnvName, def.Key) {
|
||||||
|
row := describeRow{
|
||||||
|
EnvName: def.EnvName,
|
||||||
|
Key: def.Key,
|
||||||
|
Label: def.Label,
|
||||||
|
Type: def.Type,
|
||||||
|
Editable: def.Editable,
|
||||||
|
HardLimit: def.HardLimit,
|
||||||
|
Minimum: def.Minimum,
|
||||||
|
}
|
||||||
|
if loadErr == nil {
|
||||||
|
for _, r := range cfg.SettingRows() {
|
||||||
|
if r.Definition.Key == def.Key {
|
||||||
|
row.Value = r.Value
|
||||||
|
row.Source = string(r.Source)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printDescribeRow(row)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extras := buildExtraEnvRows(true)
|
||||||
|
for _, er := range extras {
|
||||||
|
if matchEnv(query, er.EnvName, er.Key) {
|
||||||
|
row := describeRow{
|
||||||
|
EnvName: er.EnvName,
|
||||||
|
Key: er.Key,
|
||||||
|
Label: er.Label,
|
||||||
|
Type: er.Type,
|
||||||
|
Editable: er.Editable,
|
||||||
|
HardLimit: er.HardLimit,
|
||||||
|
Minimum: er.Minimum,
|
||||||
|
Default: er.Default,
|
||||||
|
}
|
||||||
|
printDescribeRow(row)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("no environment variable found matching: %s\n\nUse 'warpbox env ls' to list all available options.", query)
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchEnv(query, envName, key string) bool {
|
||||||
|
return strings.EqualFold(query, envName) || strings.EqualFold(query, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printDescribeRow(r describeRow) {
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
fmt.Fprintf(w, "Environment Variable:\t%s\n", r.EnvName)
|
||||||
|
fmt.Fprintf(w, "Setting Key:\t%s\n", r.Key)
|
||||||
|
fmt.Fprintf(w, "Label:\t%s\n", r.Label)
|
||||||
|
fmt.Fprintf(w, "Type:\t%s\n", r.Type)
|
||||||
|
fmt.Fprintf(w, "Editable (runtime):\t%v\n", r.Editable)
|
||||||
|
fmt.Fprintf(w, "Hard Limit:\t%v\n", r.HardLimit)
|
||||||
|
if r.Minimum > 0 {
|
||||||
|
fmt.Fprintf(w, "Minimum:\t%d\n", r.Minimum)
|
||||||
|
}
|
||||||
|
if r.Default != "" {
|
||||||
|
fmt.Fprintf(w, "Default:\t%s\n", r.Default)
|
||||||
|
}
|
||||||
|
if r.Value != "" {
|
||||||
|
fmt.Fprintf(w, "Current Value:\t%s\n", r.Value)
|
||||||
|
}
|
||||||
|
if r.Source != "" {
|
||||||
|
fmt.Fprintf(w, "Source:\t%s\n", r.Source)
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
}
|
||||||
80
cmd/cmd_format.go
Normal file
80
cmd/cmd_format.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/tabwriter"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"warpbox/lib/boxstore"
|
||||||
|
"warpbox/lib/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func formatBoxSummariesTable(summaries []models.BoxSummary) error {
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
fmt.Fprintln(w, "ID\tFiles\tSize\tCreated\tExpires\tPassword\tOne-Time\tExpired")
|
||||||
|
for _, s := range summaries {
|
||||||
|
expires := "-"
|
||||||
|
if !s.ExpiresAt.IsZero() {
|
||||||
|
expires = s.ExpiresAt.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
created := s.CreatedAt.Format("2006-01-02 15:04:05")
|
||||||
|
fmt.Fprintf(w, "%s\t%d\t%s\t%s\t%s\t%v\t%v\t%v\n",
|
||||||
|
s.ID, s.FileCount, s.TotalSizeLabel, created, expires,
|
||||||
|
s.PasswordProtected, s.OneTimeDownload, s.Expired)
|
||||||
|
}
|
||||||
|
return w.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatBoxSummariesJSON(summaries []models.BoxSummary) error {
|
||||||
|
type summaryOut struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
FileCount int `json:"file_count"`
|
||||||
|
TotalSize int64 `json:"total_size"`
|
||||||
|
TotalSizeLabel string `json:"total_size_label"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
Expired bool `json:"expired"`
|
||||||
|
OneTimeDownload bool `json:"one_time_download"`
|
||||||
|
PasswordProtected bool `json:"password_protected"`
|
||||||
|
}
|
||||||
|
out := make([]summaryOut, len(summaries))
|
||||||
|
for i, s := range summaries {
|
||||||
|
out[i] = summaryOut{
|
||||||
|
ID: s.ID, FileCount: s.FileCount, TotalSize: s.TotalSize,
|
||||||
|
TotalSizeLabel: s.TotalSizeLabel, CreatedAt: s.CreatedAt,
|
||||||
|
ExpiresAt: s.ExpiresAt, Expired: s.Expired,
|
||||||
|
OneTimeDownload: s.OneTimeDownload, PasswordProtected: s.PasswordProtected,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printBoxSummary(s *models.BoxSummary) {
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
fmt.Fprintf(w, "ID:\t%s\n", s.ID)
|
||||||
|
fmt.Fprintf(w, "Files:\t%d\n", s.FileCount)
|
||||||
|
fmt.Fprintf(w, "Total Size:\t%s\n", s.TotalSizeLabel)
|
||||||
|
if !s.CreatedAt.IsZero() {
|
||||||
|
fmt.Fprintf(w, "Created:\t%s\n", s.CreatedAt.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
if !s.ExpiresAt.IsZero() {
|
||||||
|
fmt.Fprintf(w, "Expires:\t%s\n", s.ExpiresAt.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "Expired:\t%v\n", s.Expired)
|
||||||
|
fmt.Fprintf(w, "Password Protected:\t%v\n", s.PasswordProtected)
|
||||||
|
fmt.Fprintf(w, "One-Time Download:\t%v\n", s.OneTimeDownload)
|
||||||
|
w.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func printRetentionOptions() {
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
fmt.Fprintln(w, "Key\tLabel\tSeconds")
|
||||||
|
for _, opt := range boxstore.RetentionOptions() {
|
||||||
|
fmt.Fprintf(w, "%s\t%s\t%d\n", opt.Key, opt.Label, opt.Seconds)
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
}
|
||||||
21
cmd/cmd_run.go
Normal file
21
cmd/cmd_run.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"warpbox/lib/server"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newRunCommand() *cobra.Command {
|
||||||
|
var addr string
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "run",
|
||||||
|
Short: "Run the HTTP server",
|
||||||
|
Long: "Run the WarpBox HTTP server.",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return server.Run(addr)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVar(&addr, "addr", ":8080", "HTTP server address")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
17
cmd/main.go
17
cmd/main.go
@@ -5,8 +5,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"warpbox/lib/server"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -23,17 +21,8 @@ func newRootCommand() *cobra.Command {
|
|||||||
Long: "WarpBox provides commands for running and managing the WarpBox service.",
|
Long: "WarpBox provides commands for running and managing the WarpBox service.",
|
||||||
}
|
}
|
||||||
|
|
||||||
var addr string
|
rootCmd.AddCommand(newRunCommand())
|
||||||
runCmd := &cobra.Command{
|
rootCmd.AddCommand(newBoxCommand())
|
||||||
Use: "run",
|
rootCmd.AddCommand(newEnvCommand())
|
||||||
Short: "Run the HTTP server",
|
|
||||||
Long: "Run the WarpBox HTTP server. The root endpoint responds with ok.",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
return server.Run(addr)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
runCmd.Flags().StringVar(&addr, "addr", ":8080", "HTTP server address")
|
|
||||||
|
|
||||||
rootCmd.AddCommand(runCmd)
|
|
||||||
return rootCmd
|
return rootCmd
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user