Files
warpbox-dev/backend/libs/config/config.go
Daniel Legt f1c67c455b feat(config): allow -1 to represent unlimited upload limits
Introduce support for configuring unlimited upload limits by allowing -1
as a valid value for anonymous and user upload MB limits.

Changes include:
- Added `envMegabytesLimitFloat` and helper functions to parse and validate limits where -1 is allowed.
- Updated validation logic to accept -1 for `AnonymousMaxUploadMB`, `AnonymousDailyUploadMB`, and `UserDailyUploadMB`.
- Added a test case to verify unlimited upload policy behavior.
2026-05-31 14:01:38 +03:00

293 lines
8.8 KiB
Go

package config
import (
"fmt"
"math"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
type Config struct {
AppName string
Environment string
Addr string
BaseURL string
DataDir string
AdminToken string
StaticDir string
TemplateDir string
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
JobsEnabled bool
CleanupEnabled bool
CleanupEvery time.Duration
ThumbnailEnabled bool
ThumbnailEvery time.Duration
MaxUploadSize int64
DefaultSettings SettingsDefaults
}
type SettingsDefaults struct {
AnonymousUploadsEnabled bool
AnonymousMaxUploadMB float64
AnonymousDailyUploadMB float64
UserDailyUploadMB float64
DefaultUserStorageMB float64
UsageRetentionDays int
LocalStorageMaxGB float64
AnonymousMaxDays int
UserMaxDays int
AnonymousDailyBoxes int
UserDailyBoxes int
AnonymousActiveBoxes int
UserActiveBoxes int
ShortWindowRequests int
ShortWindowSeconds int
AnonymousStorageBackend string
UserStorageBackend string
}
func Load() (Config, error) {
cfg := Config{
AppName: envString("WARPBOX_APP_NAME", "warpbox.dev"),
Environment: envString("WARPBOX_ENV", "development"),
Addr: envString("WARPBOX_ADDR", ":8080"),
BaseURL: strings.TrimRight(envString("WARPBOX_BASE_URL", "http://localhost:8080"), "/"),
DataDir: envString("WARPBOX_DATA_DIR", defaultPath("data")),
AdminToken: envString("WARPBOX_ADMIN_TOKEN", ""),
StaticDir: envString("WARPBOX_STATIC_DIR", defaultPath("static")),
TemplateDir: envString("WARPBOX_TEMPLATE_DIR", defaultPath("templates")),
ReadTimeout: envDuration("WARPBOX_READ_TIMEOUT", 15*time.Second),
WriteTimeout: envDuration("WARPBOX_WRITE_TIMEOUT", 60*time.Second),
IdleTimeout: envDuration("WARPBOX_IDLE_TIMEOUT", 120*time.Second),
JobsEnabled: envBool("WARPBOX_JOBS_ENABLED", true),
CleanupEnabled: envBool("WARPBOX_CLEANUP_ENABLED", true),
CleanupEvery: envDuration("WARPBOX_CLEANUP_EVERY", time.Hour),
ThumbnailEnabled: envBool("WARPBOX_THUMBNAIL_ENABLED", true),
ThumbnailEvery: envDuration("WARPBOX_THUMBNAIL_EVERY", time.Minute),
MaxUploadSize: envMegabytes("WARPBOX_MAX_UPLOAD_SIZE_MB", 2048), // 2 GiB default.
DefaultSettings: SettingsDefaults{
AnonymousUploadsEnabled: envBool("WARPBOX_ANONYMOUS_UPLOADS_ENABLED", true),
AnonymousMaxUploadMB: envMegabytesLimitFloat("WARPBOX_ANONYMOUS_MAX_UPLOAD_MB", 512),
AnonymousDailyUploadMB: envMegabytesLimitFloat("WARPBOX_ANONYMOUS_DAILY_UPLOAD_MB", 2048),
UserDailyUploadMB: envMegabytesLimitFloat("WARPBOX_USER_DAILY_UPLOAD_MB", 8192),
DefaultUserStorageMB: envMegabytesFloat("WARPBOX_DEFAULT_USER_STORAGE_MB", 51200),
UsageRetentionDays: envInt("WARPBOX_USAGE_RETENTION_DAYS", 30),
LocalStorageMaxGB: envGigabytesFloat("WARPBOX_LOCAL_STORAGE_MAX_GB", 100),
AnonymousMaxDays: envInt("WARPBOX_ANONYMOUS_MAX_DAYS", 30),
UserMaxDays: envInt("WARPBOX_USER_MAX_DAYS", 90),
AnonymousDailyBoxes: envInt("WARPBOX_ANONYMOUS_DAILY_BOXES", 100),
UserDailyBoxes: envInt("WARPBOX_USER_DAILY_BOXES", 250),
AnonymousActiveBoxes: envInt("WARPBOX_ANONYMOUS_ACTIVE_BOXES", 500),
UserActiveBoxes: envInt("WARPBOX_USER_ACTIVE_BOXES", 1000),
ShortWindowRequests: envInt("WARPBOX_SHORT_WINDOW_REQUESTS", 60),
ShortWindowSeconds: envInt("WARPBOX_SHORT_WINDOW_SECONDS", 60),
AnonymousStorageBackend: envString("WARPBOX_ANONYMOUS_STORAGE_BACKEND", "local"),
UserStorageBackend: envString("WARPBOX_USER_STORAGE_BACKEND", "local"),
},
}
if cfg.BaseURL == "" {
return Config{}, fmt.Errorf("WARPBOX_BASE_URL cannot be empty")
}
if cfg.MaxUploadSize <= 0 {
return Config{}, fmt.Errorf("WARPBOX_MAX_UPLOAD_SIZE_MB must be positive")
}
if !validUnlimitedMegabyteLimit(cfg.DefaultSettings.AnonymousMaxUploadMB) ||
!validUnlimitedMegabyteLimit(cfg.DefaultSettings.AnonymousDailyUploadMB) ||
!validUnlimitedMegabyteLimit(cfg.DefaultSettings.UserDailyUploadMB) ||
cfg.DefaultSettings.DefaultUserStorageMB <= 0 ||
cfg.DefaultSettings.UsageRetentionDays <= 0 ||
cfg.DefaultSettings.LocalStorageMaxGB <= 0 ||
cfg.DefaultSettings.AnonymousMaxDays <= 0 ||
cfg.DefaultSettings.UserMaxDays <= 0 ||
cfg.DefaultSettings.AnonymousDailyBoxes <= 0 ||
cfg.DefaultSettings.UserDailyBoxes <= 0 ||
cfg.DefaultSettings.AnonymousActiveBoxes <= 0 ||
cfg.DefaultSettings.UserActiveBoxes <= 0 ||
cfg.DefaultSettings.ShortWindowRequests <= 0 ||
cfg.DefaultSettings.ShortWindowSeconds <= 0 {
return Config{}, fmt.Errorf("upload policy settings must be positive, with -1 allowed for upload MB limits")
}
return cfg, nil
}
func defaultPath(name string) string {
candidates := []string{
filepath.Join("backend", name),
name,
}
for _, candidate := range candidates {
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
return candidate
}
}
return filepath.Join("backend", name)
}
func envString(key, fallback string) string {
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
return value
}
return fallback
}
func envDuration(key string, fallback time.Duration) time.Duration {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
parsed, err := time.ParseDuration(value)
if err != nil {
return fallback
}
return parsed
}
func envBool(key string, fallback bool) bool {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
parsed, err := strconv.ParseBool(value)
if err != nil {
return fallback
}
return parsed
}
func envInt(key string, fallback int) int {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
parsed, err := strconv.Atoi(value)
if err != nil {
return fallback
}
return parsed
}
func envMegabytes(key string, fallback float64) int64 {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return megabytesToBytes(fallback)
}
parsed, err := parseMegabytes(value)
if err != nil {
return megabytesToBytes(fallback)
}
return parsed
}
func envMegabytesFloat(key string, fallback float64) float64 {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
parsed, err := parseMegabytesFloat(value)
if err != nil {
return fallback
}
return parsed
}
func envMegabytesLimitFloat(key string, fallback float64) float64 {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
parsed, err := parseMegabytesLimitFloat(value)
if err != nil {
return fallback
}
return parsed
}
func envGigabytesFloat(key string, fallback float64) float64 {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
normalized := strings.TrimSpace(value)
normalized = strings.TrimSuffix(normalized, "GB")
normalized = strings.TrimSuffix(normalized, "Gb")
normalized = strings.TrimSuffix(normalized, "gb")
normalized = strings.TrimSpace(normalized)
parsed, err := strconv.ParseFloat(normalized, 64)
if err != nil || parsed <= 0 {
return fallback
}
return parsed
}
func parseMegabytes(value string) (int64, error) {
sizeMB, err := parseMegabytesFloat(value)
if err != nil {
return 0, err
}
return megabytesToBytes(sizeMB), nil
}
func parseMegabytesFloat(value string) (float64, error) {
normalized := strings.TrimSpace(value)
normalized = strings.TrimSuffix(normalized, "MB")
normalized = strings.TrimSuffix(normalized, "Mb")
normalized = strings.TrimSuffix(normalized, "mb")
normalized = strings.TrimSpace(normalized)
sizeMB, err := strconv.ParseFloat(normalized, 64)
if err != nil {
return 0, fmt.Errorf("invalid megabyte value %q: %w", value, err)
}
if sizeMB <= 0 {
return 0, fmt.Errorf("megabyte value must be positive")
}
return sizeMB, nil
}
func parseMegabytesLimitFloat(value string) (float64, error) {
sizeMB, err := parseMegabytesFloatAllowNegativeOne(value)
if err != nil {
return 0, err
}
if !validUnlimitedMegabyteLimit(sizeMB) {
return 0, fmt.Errorf("megabyte value must be positive or -1 for unlimited")
}
return sizeMB, nil
}
func parseMegabytesFloatAllowNegativeOne(value string) (float64, error) {
normalized := strings.TrimSpace(value)
normalized = strings.TrimSuffix(normalized, "MB")
normalized = strings.TrimSuffix(normalized, "Mb")
normalized = strings.TrimSuffix(normalized, "mb")
normalized = strings.TrimSpace(normalized)
sizeMB, err := strconv.ParseFloat(normalized, 64)
if err != nil {
return 0, fmt.Errorf("invalid megabyte value %q: %w", value, err)
}
return sizeMB, nil
}
func validUnlimitedMegabyteLimit(value float64) bool {
return value > 0 || value == -1
}
func megabytesToBytes(sizeMB float64) int64 {
return int64(math.Round(sizeMB * 1024 * 1024))
}