2026-04-30 11:31:43 +03:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
2026-04-30 12:46:44 +03:00
|
|
|
"sort"
|
|
|
|
|
"strconv"
|
2026-04-30 11:31:43 +03:00
|
|
|
"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
|
2026-04-30 12:46:44 +03:00
|
|
|
var sortBy string
|
|
|
|
|
var sortOrder string
|
|
|
|
|
var filterExpired string
|
|
|
|
|
var filterPassword string
|
|
|
|
|
var filterOneTime string
|
|
|
|
|
var filterSizeMin string
|
|
|
|
|
var filterSizeMax string
|
|
|
|
|
var filterCreatedAfter string
|
|
|
|
|
var filterCreatedBefore string
|
2026-04-30 11:31:43 +03:00
|
|
|
cmd := &cobra.Command{
|
|
|
|
|
Use: "ls",
|
|
|
|
|
Aliases: []string{"list", "view"},
|
|
|
|
|
Short: "List all boxes",
|
2026-04-30 12:46:44 +03:00
|
|
|
Long: "List all boxes with optional sorting and filtering.",
|
2026-04-30 11:31:43 +03:00
|
|
|
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
|
|
|
|
|
}
|
2026-04-30 12:46:44 +03:00
|
|
|
|
|
|
|
|
// Apply filters
|
|
|
|
|
summaries = filterBoxes(summaries, filterExpired, filterPassword, filterOneTime, filterSizeMin, filterSizeMax, filterCreatedAfter, filterCreatedBefore)
|
|
|
|
|
|
|
|
|
|
if len(summaries) == 0 {
|
|
|
|
|
fmt.Println("No boxes match the given filters.")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply sorting
|
|
|
|
|
sortBoxes(summaries, sortBy, sortOrder)
|
|
|
|
|
|
2026-04-30 11:31:43 +03:00
|
|
|
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")
|
2026-04-30 12:46:44 +03:00
|
|
|
cmd.Flags().StringVar(&sortBy, "sort", "created", "Sort field: created, expires, size, files")
|
|
|
|
|
cmd.Flags().StringVar(&sortOrder, "sort-order", "desc", "Sort order: asc, desc")
|
|
|
|
|
cmd.Flags().StringVar(&filterExpired, "filter-expired", "", "Filter by expiry: yes, no, all")
|
|
|
|
|
cmd.Flags().StringVar(&filterPassword, "filter-password", "", "Filter by password: yes, no, all")
|
|
|
|
|
cmd.Flags().StringVar(&filterOneTime, "filter-one-time", "", "Filter by one-time: yes, no, all")
|
|
|
|
|
cmd.Flags().StringVar(&filterSizeMin, "filter-size-min", "", "Minimum total size in bytes (e.g. 1024, 1k, 1m, 1g)")
|
|
|
|
|
cmd.Flags().StringVar(&filterSizeMax, "filter-size-max", "", "Maximum total size in bytes (e.g. 1024, 1k, 1m, 1g)")
|
|
|
|
|
cmd.Flags().StringVar(&filterCreatedAfter, "filter-created-after", "", "Only boxes created after this time (RFC3339)")
|
|
|
|
|
cmd.Flags().StringVar(&filterCreatedBefore, "filter-created-before", "", "Only boxes created before this time (RFC3339)")
|
2026-04-30 11:31:43 +03:00
|
|
|
return cmd
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 12:46:44 +03:00
|
|
|
func filterBoxes(summaries []models.BoxSummary, filterExpired, filterPassword, filterOneTime, filterSizeMin, filterSizeMax, filterCreatedAfter, filterCreatedBefore string) []models.BoxSummary {
|
|
|
|
|
result := make([]models.BoxSummary, 0, len(summaries))
|
|
|
|
|
|
|
|
|
|
minSize, _ := parseSizeFilter(filterSizeMin)
|
|
|
|
|
maxSize, _ := parseSizeFilter(filterSizeMax)
|
|
|
|
|
createdAfter, _ := time.Parse(time.RFC3339, filterCreatedAfter)
|
|
|
|
|
createdBefore, _ := time.Parse(time.RFC3339, filterCreatedBefore)
|
|
|
|
|
|
|
|
|
|
for _, s := range summaries {
|
|
|
|
|
if filterExpired != "" && filterExpired != "all" {
|
|
|
|
|
match := "no"
|
|
|
|
|
if s.Expired {
|
|
|
|
|
match = "yes"
|
|
|
|
|
}
|
|
|
|
|
if match != filterExpired {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if filterPassword != "" && filterPassword != "all" {
|
|
|
|
|
match := "no"
|
|
|
|
|
if s.PasswordProtected {
|
|
|
|
|
match = "yes"
|
|
|
|
|
}
|
|
|
|
|
if match != filterPassword {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if filterOneTime != "" && filterOneTime != "all" {
|
|
|
|
|
match := "no"
|
|
|
|
|
if s.OneTimeDownload {
|
|
|
|
|
match = "yes"
|
|
|
|
|
}
|
|
|
|
|
if match != filterOneTime {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if minSize > 0 && s.TotalSize < minSize {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if maxSize > 0 && s.TotalSize > maxSize {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if !createdAfter.IsZero() && s.CreatedAt.Before(createdAfter) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if !createdBefore.IsZero() && !s.CreatedAt.Before(createdBefore) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
result = append(result, s)
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseSizeFilter(s string) (int64, error) {
|
|
|
|
|
if s == "" {
|
|
|
|
|
return 0, nil
|
|
|
|
|
}
|
|
|
|
|
s = strings.TrimSpace(s)
|
|
|
|
|
lower := strings.ToLower(s)
|
|
|
|
|
|
|
|
|
|
multiplier := int64(1)
|
|
|
|
|
switch {
|
|
|
|
|
case strings.HasSuffix(lower, "g"):
|
|
|
|
|
multiplier = 1024 * 1024 * 1024
|
|
|
|
|
s = strings.TrimSuffix(lower, "g")
|
|
|
|
|
case strings.HasSuffix(lower, "m"):
|
|
|
|
|
multiplier = 1024 * 1024
|
|
|
|
|
s = strings.TrimSuffix(lower, "m")
|
|
|
|
|
case strings.HasSuffix(lower, "k"):
|
|
|
|
|
multiplier = 1024
|
|
|
|
|
s = strings.TrimSuffix(lower, "k")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val, err := strconv.ParseInt(s, 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, fmt.Errorf("invalid size filter: %s", s)
|
|
|
|
|
}
|
|
|
|
|
return val * multiplier, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func sortBoxes(summaries []models.BoxSummary, sortBy, sortOrder string) {
|
|
|
|
|
reverse := false
|
|
|
|
|
if strings.EqualFold(sortOrder, "desc") {
|
|
|
|
|
reverse = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sort.SliceStable(summaries, func(i, j int) bool {
|
|
|
|
|
var less bool
|
|
|
|
|
switch strings.ToLower(sortBy) {
|
|
|
|
|
case "size":
|
|
|
|
|
less = summaries[i].TotalSize < summaries[j].TotalSize
|
|
|
|
|
case "files":
|
|
|
|
|
less = summaries[i].FileCount < summaries[j].FileCount
|
|
|
|
|
case "expires":
|
|
|
|
|
// Boxes with no expiry go last
|
|
|
|
|
iZero := summaries[i].ExpiresAt.IsZero()
|
|
|
|
|
jZero := summaries[j].ExpiresAt.IsZero()
|
|
|
|
|
if iZero && jZero {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if iZero {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if jZero {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
less = summaries[i].ExpiresAt.Before(summaries[j].ExpiresAt)
|
|
|
|
|
case "created", "":
|
|
|
|
|
less = summaries[i].CreatedAt.Before(summaries[j].CreatedAt)
|
|
|
|
|
default:
|
|
|
|
|
less = summaries[i].ID < summaries[j].ID
|
|
|
|
|
}
|
|
|
|
|
if reverse {
|
|
|
|
|
return !less
|
|
|
|
|
}
|
|
|
|
|
return less
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 11:31:43 +03:00
|
|
|
func newBoxViewCommand() *cobra.Command {
|
|
|
|
|
var uploadRoot string
|
2026-04-30 12:46:44 +03:00
|
|
|
var asJSON bool
|
2026-04-30 11:31:43 +03:00
|
|
|
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)
|
|
|
|
|
}
|
2026-04-30 12:46:44 +03:00
|
|
|
if asJSON {
|
|
|
|
|
return formatBoxSummaryJSON(&summary)
|
|
|
|
|
}
|
2026-04-30 11:31:43 +03:00
|
|
|
printBoxSummary(&summary)
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
cmd.Flags().StringVar(&uploadRoot, "upload-root", "", "Override upload root directory")
|
2026-04-30 12:46:44 +03:00
|
|
|
cmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON")
|
2026-04-30 11:31:43 +03:00
|
|
|
return cmd
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newBoxInspectCommand() *cobra.Command {
|
|
|
|
|
var uploadRoot string
|
|
|
|
|
var full bool
|
2026-04-30 12:46:44 +03:00
|
|
|
var asJSON bool
|
2026-04-30 11:31:43 +03:00
|
|
|
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)")
|
2026-04-30 12:46:44 +03:00
|
|
|
cmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON (default for inspect)")
|
|
|
|
|
_ = asJSON // inspect is always JSON; flag kept for consistency
|
2026-04-30 11:31:43 +03:00
|
|
|
return cmd
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newBoxDeleteCommand() *cobra.Command {
|
|
|
|
|
var uploadRoot string
|
|
|
|
|
var force bool
|
2026-04-30 12:46:44 +03:00
|
|
|
var asJSON bool
|
2026-04-30 11:31:43 +03:00
|
|
|
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" {
|
2026-04-30 12:46:44 +03:00
|
|
|
if asJSON {
|
|
|
|
|
fmt.Println(`{"deleted": false, "reason": "aborted"}`)
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Println("Aborted.")
|
|
|
|
|
}
|
2026-04-30 11:31:43 +03:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := boxstore.DeleteBox(boxID); err != nil {
|
2026-04-30 12:46:44 +03:00
|
|
|
if asJSON {
|
|
|
|
|
fmt.Printf(`{"deleted": false, "error": "%s"}\n`, strings.ReplaceAll(err.Error(), `"`, `\"`))
|
|
|
|
|
} else {
|
|
|
|
|
return fmt.Errorf("failed to delete box %s: %w", boxID, err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if asJSON {
|
|
|
|
|
fmt.Printf(`{"deleted": true, "box_id": "%s"}\n`, boxID)
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Printf("Box %s deleted.\n", boxID)
|
2026-04-30 11:31:43 +03:00
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
cmd.Flags().StringVar(&uploadRoot, "upload-root", "", "Override upload root directory")
|
|
|
|
|
cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation prompt")
|
2026-04-30 12:46:44 +03:00
|
|
|
cmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON")
|
2026-04-30 11:31:43 +03:00
|
|
|
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
|
2026-04-30 12:46:44 +03:00
|
|
|
var asJSON bool
|
2026-04-30 11:31:43 +03:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 12:46:44 +03:00
|
|
|
if asJSON {
|
|
|
|
|
return formatChangeResultJSON(boxID, manifest)
|
|
|
|
|
}
|
2026-04-30 11:31:43 +03:00
|
|
|
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)")
|
2026-04-30 12:46:44 +03:00
|
|
|
cmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON")
|
2026-04-30 11:31:43 +03:00
|
|
|
|
|
|
|
|
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
|
2026-04-30 12:46:44 +03:00
|
|
|
var asJSON bool
|
2026-04-30 11:31:43 +03:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 12:46:44 +03:00
|
|
|
if asJSON {
|
|
|
|
|
return formatBoxGetJSON(boxID, manifest)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 11:31:43 +03:00
|
|
|
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")
|
2026-04-30 12:46:44 +03:00
|
|
|
cmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON")
|
2026-04-30 11:31:43 +03:00
|
|
|
return cmd
|
|
|
|
|
}
|