package config import ( "fmt" "os" "path/filepath" "strconv" "strings" "time" ) type Config struct { Addr string DatabaseURL string RedisURL string JWTSecret []byte AdminUser string AdminPassword string BooksDir string CacheDir string ScanInterval time.Duration UploadMaxMB int64 TrustedProxies []string } func Load() (*Config, error) { env := func(k, def string) string { if v := os.Getenv(k); v != "" { return v } return def } envList := func(k, def string) []string { v := env(k, def) out := make([]string, 0, len(strings.Split(v, ","))) for _, s := range strings.Split(v, ",") { if s = strings.TrimSpace(s); s != "" { out = append(out, s) } } return out } scanSec, err := strconv.Atoi(env("SCAN_INTERVAL_SEC", "60")) if err != nil { return nil, fmt.Errorf("SCAN_INTERVAL_SEC: %w", err) } uploadMB, err := strconv.ParseInt(env("UPLOAD_MAX_MB", "200"), 10, 64) if err != nil { return nil, fmt.Errorf("UPLOAD_MAX_MB: %w", err) } secret := os.Getenv("JWT_SECRET") if secret == "" { return nil, fmt.Errorf("JWT_SECRET required") } // resolveDir: macOS 开发机上 /var、/tmp 是指向 /private 的软链, // 启动时解析一次才能让 root_path 前缀校验对上真实路径;目录不存在/出错则保留原值(Docker 路径不受影响) resolveDir := func(dir string) string { if r, e := filepath.EvalSymlinks(dir); e == nil { return r } return dir } return &Config{ Addr: env("ADDR", ":8080"), DatabaseURL: env("DATABASE_URL", ""), RedisURL: env("REDIS_URL", ""), JWTSecret: []byte(secret), AdminUser: env("ADMIN_USER", ""), AdminPassword: env("ADMIN_PASSWORD", ""), BooksDir: resolveDir(env("BOOKS_DIR", "/data/books")), CacheDir: resolveDir(env("CACHE_DIR", "/data/cache")), ScanInterval: time.Duration(scanSec) * time.Second, UploadMaxMB: uploadMB, TrustedProxies: envList("TRUSTED_PROXY_CIDRS", "172.16.0.0/12"), }, nil }