223 lines
7.0 KiB
Go
223 lines
7.0 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Config struct {
|
|
Environment string
|
|
Address string
|
|
DataDir string
|
|
PublicURL string
|
|
AdminEmail string
|
|
AdminName string
|
|
AdminPassword string
|
|
DeveloperToken string
|
|
AllowLegacyToken bool
|
|
AllowedOrigins []string
|
|
AllowedHosts []string
|
|
ForceHTTPS bool
|
|
TrustProxy bool
|
|
SessionDuration time.Duration
|
|
MaxUploadBytes int64
|
|
RateLimitPerMinute int
|
|
ReadHeaderTimeout time.Duration
|
|
ReadTimeout time.Duration
|
|
WriteTimeout time.Duration
|
|
IdleTimeout time.Duration
|
|
ShutdownTimeout time.Duration
|
|
LogLevel string
|
|
}
|
|
|
|
func Load() (Config, error) {
|
|
environment := strings.ToLower(strings.TrimSpace(envOr("SCOUT_ENV", "development")))
|
|
if environment == "" {
|
|
environment = "development"
|
|
}
|
|
if environment != "development" && environment != "test" && environment != "production" {
|
|
return Config{}, errors.New("SCOUT_ENV 只能是 development、test 或 production")
|
|
}
|
|
|
|
sessionDuration, err := durationEnv("SCOUT_SESSION_DURATION", 24*time.Hour)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
maxUploadBytes, err := bytesEnv("SCOUT_MAX_UPLOAD_BYTES", 512<<20)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
rateLimit, err := intEnv("SCOUT_RATE_LIMIT_PER_MINUTE", 120)
|
|
if err != nil || rateLimit < 0 {
|
|
return Config{}, errors.New("SCOUT_RATE_LIMIT_PER_MINUTE 必须是大于等于 0 的整数")
|
|
}
|
|
|
|
cfg := Config{
|
|
Environment: environment,
|
|
Address: envOr("SCOUT_ADDR", ":8080"),
|
|
DataDir: envOr("SCOUT_DATA_DIR", "data"),
|
|
PublicURL: strings.TrimRight(strings.TrimSpace(os.Getenv("SCOUT_PUBLIC_URL")), "/"),
|
|
AdminEmail: strings.TrimSpace(os.Getenv("SCOUT_ADMIN_EMAIL")),
|
|
AdminName: strings.TrimSpace(os.Getenv("SCOUT_ADMIN_NAME")),
|
|
AdminPassword: os.Getenv("SCOUT_ADMIN_PASSWORD"),
|
|
DeveloperToken: os.Getenv("SCOUT_DEVELOPER_TOKEN"),
|
|
AllowedOrigins: csvEnv("SCOUT_ALLOWED_ORIGINS"),
|
|
AllowedHosts: csvEnv("SCOUT_ALLOWED_HOSTS"),
|
|
ForceHTTPS: boolEnv("SCOUT_FORCE_HTTPS", environment == "production"),
|
|
TrustProxy: boolEnv("SCOUT_TRUST_PROXY", false),
|
|
SessionDuration: sessionDuration,
|
|
MaxUploadBytes: maxUploadBytes,
|
|
RateLimitPerMinute: rateLimit,
|
|
ReadHeaderTimeout: durationDefault("SCOUT_READ_HEADER_TIMEOUT", 10*time.Second),
|
|
ReadTimeout: durationDefault("SCOUT_READ_TIMEOUT", 60*time.Second),
|
|
WriteTimeout: durationDefault("SCOUT_WRITE_TIMEOUT", 10*time.Minute),
|
|
IdleTimeout: durationDefault("SCOUT_IDLE_TIMEOUT", 2*time.Minute),
|
|
ShutdownTimeout: durationDefault("SCOUT_SHUTDOWN_TIMEOUT", 15*time.Second),
|
|
LogLevel: strings.ToLower(envOr("SCOUT_LOG_LEVEL", "info")),
|
|
}
|
|
cfg.AllowLegacyToken = boolEnv("SCOUT_ALLOW_LEGACY_TOKEN", environment != "production")
|
|
|
|
if err := cfg.validate(); err != nil {
|
|
return Config{}, err
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func (cfg Config) validate() error {
|
|
if strings.TrimSpace(cfg.Address) == "" || strings.TrimSpace(cfg.DataDir) == "" {
|
|
return errors.New("SCOUT_ADDR 和 SCOUT_DATA_DIR 不能为空")
|
|
}
|
|
if cfg.SessionDuration <= 0 || cfg.MaxUploadBytes <= 0 {
|
|
return errors.New("会话时长和上传大小必须大于 0")
|
|
}
|
|
if cfg.LogLevel != "debug" && cfg.LogLevel != "info" && cfg.LogLevel != "warn" && cfg.LogLevel != "error" {
|
|
return errors.New("SCOUT_LOG_LEVEL 只能是 debug、info、warn 或 error")
|
|
}
|
|
if cfg.PublicURL != "" {
|
|
parsed, err := url.Parse(cfg.PublicURL)
|
|
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
|
return errors.New("SCOUT_PUBLIC_URL 必须是完整的 HTTP 或 HTTPS 地址")
|
|
}
|
|
if cfg.Environment == "production" && parsed.Scheme != "https" {
|
|
return errors.New("生产环境的 SCOUT_PUBLIC_URL 必须使用 HTTPS")
|
|
}
|
|
}
|
|
if cfg.Environment != "production" {
|
|
return nil
|
|
}
|
|
if cfg.AdminEmail == "" || cfg.AdminPassword == "" {
|
|
return errors.New("生产环境必须配置 SCOUT_ADMIN_EMAIL 和 SCOUT_ADMIN_PASSWORD")
|
|
}
|
|
if cfg.AdminPassword == "Admin-Test-123" {
|
|
return errors.New("生产环境不能使用本地测试管理员密码")
|
|
}
|
|
if cfg.DeveloperToken != "" && !cfg.AllowLegacyToken {
|
|
return errors.New("生产环境默认禁止兼容开发者令牌;如确需启用,请设置 SCOUT_ALLOW_LEGACY_TOKEN=true")
|
|
}
|
|
if len(cfg.AllowedOrigins) == 0 {
|
|
return errors.New("生产环境必须配置 SCOUT_ALLOWED_ORIGINS")
|
|
}
|
|
if len(cfg.AllowedHosts) == 0 {
|
|
return errors.New("生产环境必须配置 SCOUT_ALLOWED_HOSTS")
|
|
}
|
|
if cfg.PublicURL == "" {
|
|
return errors.New("生产环境必须配置 SCOUT_PUBLIC_URL")
|
|
}
|
|
if os.Getenv("SCOUT_SMTP_HOST") == "" || os.Getenv("SCOUT_SMTP_FROM") == "" {
|
|
return errors.New("生产环境必须配置 SCOUT_SMTP_HOST 和 SCOUT_SMTP_FROM")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func envOr(name, fallback string) string {
|
|
if value := os.Getenv(name); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func csvEnv(name string) []string {
|
|
values := make([]string, 0)
|
|
for _, value := range strings.Split(os.Getenv(name), ",") {
|
|
value = strings.TrimSpace(value)
|
|
if value != "" {
|
|
values = append(values, strings.TrimRight(value, "/"))
|
|
}
|
|
}
|
|
return values
|
|
}
|
|
|
|
func boolEnv(name string, fallback bool) bool {
|
|
value := strings.TrimSpace(os.Getenv(name))
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
parsed, err := strconv.ParseBool(value)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func intEnv(name string, fallback int) (int, error) {
|
|
value := strings.TrimSpace(os.Getenv(name))
|
|
if value == "" {
|
|
return fallback, nil
|
|
}
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%s 必须是整数", name)
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func durationEnv(name string, fallback time.Duration) (time.Duration, error) {
|
|
value := strings.TrimSpace(os.Getenv(name))
|
|
if value == "" {
|
|
return fallback, nil
|
|
}
|
|
parsed, err := time.ParseDuration(value)
|
|
if err != nil || parsed <= 0 {
|
|
return 0, fmt.Errorf("%s 必须是大于 0 的时间长度,例如 24h 或 30s", name)
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func durationDefault(name string, fallback time.Duration) time.Duration {
|
|
value, err := durationEnv(name, fallback)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
return value
|
|
}
|
|
|
|
func bytesEnv(name string, fallback int64) (int64, error) {
|
|
value := strings.TrimSpace(os.Getenv(name))
|
|
if value == "" {
|
|
return fallback, nil
|
|
}
|
|
multiplier := int64(1)
|
|
upper := strings.ToUpper(value)
|
|
switch {
|
|
case strings.HasSuffix(upper, "KB"):
|
|
multiplier = 1 << 10
|
|
value = strings.TrimSpace(value[:len(value)-2])
|
|
case strings.HasSuffix(upper, "MB"):
|
|
multiplier = 1 << 20
|
|
value = strings.TrimSpace(value[:len(value)-2])
|
|
case strings.HasSuffix(upper, "GB"):
|
|
multiplier = 1 << 30
|
|
value = strings.TrimSpace(value[:len(value)-2])
|
|
}
|
|
number, err := strconv.ParseInt(value, 10, 64)
|
|
if err != nil || number <= 0 {
|
|
return 0, fmt.Errorf("%s 必须是正整数或带 KB、MB、GB 后缀的大小", name)
|
|
}
|
|
return number * multiplier, nil
|
|
}
|