// Package config reads all settings from environment variables. // Names and defaults match the table in the README. package config import ( "fmt" "log" "net/url" "os" "path/filepath" "strconv" ) type Config struct { Port int SSHPort int DataDir string OwnerDisplayName string BaseURL string PublicHTTPS bool PublicOrigin string RegistrationType string // enabled | disabled | queue RegisterQuestion string MaxUploadBytes int64 MaxUserUploadBytes int64 InlineMaxBytes int64 MaxRenderBytes int64 MaxRawDownloadBytes int64 SSHDisabled bool SSHHostKeyPath string ScannedRepoPrivate bool TrustedProxy bool RateLimitDisabled bool CommitterName string CommitterEmail string ExtraAllowedSigners string MaxTitleBytes int MaxTextBodyBytes int MaxUsernameBytes int MaxPasswordBytes int CIDockerSocket string CIMaxHistory int CIDefaultTimeout int CIMaxConcurrent int CIMaxArtifactBytes int64 MaxConcurrentArchives int } // intEnv returns def when unset or unparsable. min clamps the result; // pass it where 0 would break the feature instead of disabling it. func intEnv(key string, def, min int64) int64 { v := os.Getenv(key) if v == "" { return def } n, err := strconv.ParseInt(v, 10, 64) if err != nil { log.Printf("config: %s=%q is not a number, using %d", key, v, def) return def } if n < min { return min } return n } func strEnv(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } // boolEnv treats anything but "", "0" and "false" as true. A value that looks // like neither is almost always a typo, so it is logged. func boolEnv(key string) bool { v := os.Getenv(key) switch v { case "", "0", "false", "1", "true": default: log.Printf("config: %s=%q is not a boolean, reading it as true", key, v) } return v != "" && v != "0" && v != "false" } func Load() (*Config, error) { port := int(intEnv("PORT", 3000, 1)) owner := strEnv("OWNER_DISPLAY_NAME", "Admin") dataDir, err := filepath.Abs(strEnv("DATA_DIR", "./data")) if err != nil { return nil, err } c := &Config{ Port: port, SSHPort: int(intEnv("SSH_PORT", 2222, 1)), DataDir: dataDir, OwnerDisplayName: owner, BaseURL: strEnv("BASE_URL", fmt.Sprintf("http://localhost:%d", port)), RegistrationType: strEnv("REGISTRATION_TYPE", "enabled"), RegisterQuestion: os.Getenv("REGISTER_QUESTION"), MaxUploadBytes: intEnv("MAX_UPLOAD_BYTES", 10<<20, 0), MaxUserUploadBytes: intEnv("MAX_USER_UPLOAD_BYTES", 2<<20, 0), InlineMaxBytes: intEnv("INLINE_MAX_BYTES", 512<<10, 0), MaxRenderBytes: intEnv("MAX_RENDER_BYTES", 10<<20, 0), MaxRawDownloadBytes: intEnv("MAX_RAW_DOWNLOAD_BYTES", 0, 0), SSHDisabled: boolEnv("SSH_DISABLED"), SSHHostKeyPath: strEnv("SSH_HOST_KEY_PATH", filepath.Join(dataDir, "ssh_host_key")), ScannedRepoPrivate: os.Getenv("SCANNED_REPO_PRIVATE") != "0" && os.Getenv("SCANNED_REPO_PRIVATE") != "false", TrustedProxy: boolEnv("TRUSTED_PROXY"), RateLimitDisabled: boolEnv("RATE_LIMIT_DISABLED"), CommitterName: strEnv("COMMITTER_NAME", owner), ExtraAllowedSigners: os.Getenv("EXTRA_ALLOWED_SIGNERS_PATH"), MaxTitleBytes: int(intEnv("MAX_TITLE_BYTES", 500, 0)), MaxTextBodyBytes: int(intEnv("MAX_TEXT_BODY_BYTES", 100_000, 0)), MaxUsernameBytes: int(intEnv("MAX_USERNAME_BYTES", 64, 0)), MaxPasswordBytes: int(intEnv("MAX_PASSWORD_BYTES", 1024, 0)), CIDockerSocket: os.Getenv("CI_DOCKER_SOCKET"), CIMaxHistory: int(intEnv("CI_MAX_HISTORY", 50, 1)), CIDefaultTimeout: int(intEnv("CI_DEFAULT_TIMEOUT", 3600, 1)), CIMaxConcurrent: int(intEnv("CI_MAX_CONCURRENT", 2, 1)), CIMaxArtifactBytes: intEnv("CI_MAX_ARTIFACT_BYTES", 512<<20, 1), MaxConcurrentArchives: int(intEnv("MAX_CONCURRENT_ARCHIVE_JOBS", 2, 1)), } switch c.RegistrationType { case "enabled", "disabled", "queue": default: return nil, fmt.Errorf("REGISTRATION_TYPE %q must be enabled, disabled or queue", c.RegistrationType) } u, err := url.Parse(c.BaseURL) if err != nil || u.Host == "" { return nil, fmt.Errorf("BASE_URL %q is not a valid URL", c.BaseURL) } c.PublicHTTPS = u.Scheme == "https" c.PublicOrigin = u.Scheme + "://" + u.Host c.CommitterEmail = strEnv("COMMITTER_EMAIL", owner+"@"+u.Hostname()) return c, nil } // Derived paths under DataDir. func (c *Config) DBPath() string { return filepath.Join(c.DataDir, "hearthforge.db") } func (c *Config) ReposDir() string { return filepath.Join(c.DataDir, "repos") } func (c *Config) AvatarsDir() string { return filepath.Join(c.DataDir, "avatars") } func (c *Config) ReleasesDir() string { return filepath.Join(c.DataDir, "releases") } func (c *Config) AllowedSignersPath() string { return filepath.Join(c.DataDir, "allowed_signers") } func (c *Config) CIArtifactsDir() string { return filepath.Join(c.DataDir, "ci", "artifacts") }