package config import ( "fmt" "os" "strconv" "strings" "time" ) const ( DefaultListenAddr = "127.0.0.1:8080" DefaultDataDir = "/opt/glchat/data" DefaultLogLevel = "info" ) type Config struct { Domain string FilesDomain string InstanceName string ListenAddr string DataDir string WebRoot string DatabasePath string SQLiteCache string SQLiteMmap string LogLevel string LogFormat string ReadPoolSize int MaxUploadSize int64 TLSEnabled bool // Секреты инстанса (генерируются установщиком, AGENT.md §10.3). SessionPepper string MasterKey string TOTPEncryptionKey string SessionTTLHours int Argon2MemoryKiB int Argon2Iterations int Argon2Parallelism int Version string Commit string BuildDate string ShutdownTTL time.Duration Maintenance time.Duration } func Load() (Config, error) { cfg := Config{ Domain: env("DOMAIN", "localhost"), FilesDomain: env("FILES_DOMAIN", ""), InstanceName: env("INSTANCE_NAME", "glchat"), ListenAddr: env("LISTEN_ADDR", DefaultListenAddr), DataDir: env("DATA_DIR", DefaultDataDir), WebRoot: env("WEB_ROOT", "/app/web"), DatabasePath: env("DATABASE_PATH", ""), SQLiteCache: env("SQLITE_CACHE_SIZE", "-8000"), SQLiteMmap: env("SQLITE_MMAP_SIZE", "134217728"), LogLevel: strings.ToLower(env("LOG_LEVEL", DefaultLogLevel)), LogFormat: strings.ToLower(env("LOG_FORMAT", "json")), ReadPoolSize: envInt("SQLITE_READ_POOL", 4), MaxUploadSize: envInt64("MAX_UPLOAD_SIZE", 26214400), TLSEnabled: envBool("TLS_ENABLED", true), SessionPepper: env("SESSION_PEPPER", ""), MasterKey: env("MASTER_KEY", ""), TOTPEncryptionKey: env("TOTP_ENCRYPTION_KEY", ""), SessionTTLHours: envInt("SESSION_TTL_HOURS", 720), Argon2MemoryKiB: envInt("ARGON2_MEMORY_KIB", 19456), Argon2Iterations: envInt("ARGON2_ITERATIONS", 2), Argon2Parallelism: envInt("ARGON2_PARALLELISM", 1), Version: env("APP_VERSION", "dev"), Commit: env("APP_COMMIT", "none"), BuildDate: env("APP_BUILD_DATE", "unknown"), ShutdownTTL: time.Duration(envInt("SHUTDOWN_TIMEOUT_SECONDS", 20)) * time.Second, Maintenance: time.Duration(envInt("DB_MAINTENANCE_INTERVAL_SECONDS", 21600)) * time.Second, } if cfg.FilesDomain == "" { cfg.FilesDomain = "files." + cfg.Domain } if cfg.DatabasePath == "" { cfg.DatabasePath = cfg.DataDir + "/db/glchat.db" } if cfg.ReadPoolSize < 1 { return Config{}, fmt.Errorf("SQLITE_READ_POOL must be >= 1, got %d", cfg.ReadPoolSize) } if _, err := parseLogLevel(cfg.LogLevel); err != nil { return Config{}, err } if cfg.LogFormat != "json" && cfg.LogFormat != "text" { return Config{}, fmt.Errorf("LOG_FORMAT must be json or text, got %q", cfg.LogFormat) } return cfg, nil } // Scheme выбирается установщиком: при установке без TLS (--skip-tls, // например стенд без домена) клиент должен получать http/ws, а не https/wss. func (c Config) Scheme() string { if c.TLSEnabled { return "https" } return "http" } func (c Config) BaseURL() string { return c.Scheme() + "://" + c.Domain } func (c Config) FilesURL() string { return c.Scheme() + "://" + c.FilesDomain } func (c Config) GatewayURL() string { scheme := "ws" if c.TLSEnabled { scheme = "wss" } return scheme + "://" + c.Domain + "/gateway" } func (c Config) LogLevelNumeric() int { level, err := parseLogLevel(c.LogLevel) if err != nil { return 0 } return level } func parseLogLevel(level string) (int, error) { switch level { case "debug": return -4, nil case "info": return 0, nil case "warn", "warning": return 4, nil case "error": return 8, nil default: return 0, fmt.Errorf("unknown LOG_LEVEL %q", level) } } func env(key, fallback string) string { if v, ok := os.LookupEnv(key); ok && v != "" { return v } return fallback } func envInt(key string, fallback int) int { v, ok := os.LookupEnv(key) if !ok || v == "" { return fallback } n, err := strconv.Atoi(v) if err != nil { return fallback } return n } func envBool(key string, fallback bool) bool { v, ok := os.LookupEnv(key) if !ok || v == "" { return fallback } parsed, err := strconv.ParseBool(v) if err != nil { return fallback } return parsed } func envInt64(key string, fallback int64) int64 { v, ok := os.LookupEnv(key) if !ok || v == "" { return fallback } n, err := strconv.ParseInt(v, 10, 64) if err != nil { return fallback } return n }