package config import ( "fmt" "math" "os" "path/filepath" "strconv" "strings" "time" ) type Config struct { AppName string Environment string Addr string BaseURL string StaticDir string TemplateDir string ReadTimeout time.Duration WriteTimeout time.Duration IdleTimeout time.Duration MaxUploadSize int64 } 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"), "/"), 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), MaxUploadSize: envMegabytes("WARPBOX_MAX_UPLOAD_SIZE_MB", 2048), // 2 GiB default. } 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") } 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 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 parseMegabytes(value string) (int64, 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 megabytesToBytes(sizeMB), nil } func megabytesToBytes(sizeMB float64) int64 { return int64(math.Round(sizeMB * 1024 * 1024)) }