config.go
| 1 | // Package config reads all settings from environment variables. |
| 2 | // Names and defaults match the table in the README. |
| 3 | package config |
| 4 | |
| 5 | import ( |
| 6 | "fmt" |
| 7 | "log" |
| 8 | "net/url" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "strconv" |
| 12 | ) |
| 13 | |
| 14 | type Config struct { |
| 15 | Port int |
| 16 | SSHPort int |
| 17 | DataDir string |
| 18 | OwnerDisplayName string |
| 19 | BaseURL string |
| 20 | PublicHTTPS bool |
| 21 | PublicOrigin string |
| 22 | RegistrationType string // enabled | disabled | queue |
| 23 | RegisterQuestion string |
| 24 | MaxUploadBytes int64 |
| 25 | MaxUserUploadBytes int64 |
| 26 | InlineMaxBytes int64 |
| 27 | MaxRenderBytes int64 |
| 28 | MaxRawDownloadBytes int64 |
| 29 | SSHDisabled bool |
| 30 | SSHHostKeyPath string |
| 31 | ScannedRepoPrivate bool |
| 32 | TrustedProxy bool |
| 33 | RateLimitDisabled bool |
| 34 | CommitterName string |
| 35 | CommitterEmail string |
| 36 | ExtraAllowedSigners string |
| 37 | MaxTitleBytes int |
| 38 | MaxTextBodyBytes int |
| 39 | MaxUsernameBytes int |
| 40 | MaxPasswordBytes int |
| 41 | CIDockerSocket string |
| 42 | CIMaxHistory int |
| 43 | CIDefaultTimeout int |
| 44 | CIMaxConcurrent int |
| 45 | CIMaxArtifactBytes int64 |
| 46 | MaxConcurrentArchives int |
| 47 | } |
| 48 | |
| 49 | // intEnv returns def when unset or unparsable. min clamps the result; |
| 50 | // pass it where 0 would break the feature instead of disabling it. |
| 51 | func intEnv(key string, def, min int64) int64 { |
| 52 | v := os.Getenv(key) |
| 53 | if v == "" { |
| 54 | return def |
| 55 | } |
| 56 | n, err := strconv.ParseInt(v, 10, 64) |
| 57 | if err != nil { |
| 58 | log.Printf("config: %s=%q is not a number, using %d", key, v, def) |
| 59 | return def |
| 60 | } |
| 61 | if n < min { |
| 62 | return min |
| 63 | } |
| 64 | return n |
| 65 | } |
| 66 | |
| 67 | func strEnv(key, def string) string { |
| 68 | if v := os.Getenv(key); v != "" { |
| 69 | return v |
| 70 | } |
| 71 | return def |
| 72 | } |
| 73 | |
| 74 | // boolEnv treats anything but "", "0" and "false" as true. A value that looks |
| 75 | // like neither is almost always a typo, so it is logged. |
| 76 | func boolEnv(key string) bool { |
| 77 | v := os.Getenv(key) |
| 78 | switch v { |
| 79 | case "", "0", "false", "1", "true": |
| 80 | default: |
| 81 | log.Printf("config: %s=%q is not a boolean, reading it as true", key, v) |
| 82 | } |
| 83 | return v != "" && v != "0" && v != "false" |
| 84 | } |
| 85 | |
| 86 | func Load() (*Config, error) { |
| 87 | port := int(intEnv("PORT", 3000, 1)) |
| 88 | owner := strEnv("OWNER_DISPLAY_NAME", "Admin") |
| 89 | dataDir, err := filepath.Abs(strEnv("DATA_DIR", "./data")) |
| 90 | if err != nil { |
| 91 | return nil, err |
| 92 | } |
| 93 | c := &Config{ |
| 94 | Port: port, |
| 95 | SSHPort: int(intEnv("SSH_PORT", 2222, 1)), |
| 96 | DataDir: dataDir, |
| 97 | OwnerDisplayName: owner, |
| 98 | BaseURL: strEnv("BASE_URL", fmt.Sprintf("http://localhost:%d", port)), |
| 99 | RegistrationType: strEnv("REGISTRATION_TYPE", "enabled"), |
| 100 | RegisterQuestion: os.Getenv("REGISTER_QUESTION"), |
| 101 | MaxUploadBytes: intEnv("MAX_UPLOAD_BYTES", 10<<20, 0), |
| 102 | MaxUserUploadBytes: intEnv("MAX_USER_UPLOAD_BYTES", 2<<20, 0), |
| 103 | InlineMaxBytes: intEnv("INLINE_MAX_BYTES", 512<<10, 0), |
| 104 | MaxRenderBytes: intEnv("MAX_RENDER_BYTES", 10<<20, 0), |
| 105 | MaxRawDownloadBytes: intEnv("MAX_RAW_DOWNLOAD_BYTES", 0, 0), |
| 106 | SSHDisabled: boolEnv("SSH_DISABLED"), |
| 107 | SSHHostKeyPath: strEnv("SSH_HOST_KEY_PATH", filepath.Join(dataDir, "ssh_host_key")), |
| 108 | ScannedRepoPrivate: os.Getenv("SCANNED_REPO_PRIVATE") != "0" && os.Getenv("SCANNED_REPO_PRIVATE") != "false", |
| 109 | TrustedProxy: boolEnv("TRUSTED_PROXY"), |
| 110 | RateLimitDisabled: boolEnv("RATE_LIMIT_DISABLED"), |
| 111 | CommitterName: strEnv("COMMITTER_NAME", owner), |
| 112 | ExtraAllowedSigners: os.Getenv("EXTRA_ALLOWED_SIGNERS_PATH"), |
| 113 | MaxTitleBytes: int(intEnv("MAX_TITLE_BYTES", 500, 0)), |
| 114 | MaxTextBodyBytes: int(intEnv("MAX_TEXT_BODY_BYTES", 100_000, 0)), |
| 115 | MaxUsernameBytes: int(intEnv("MAX_USERNAME_BYTES", 64, 0)), |
| 116 | MaxPasswordBytes: int(intEnv("MAX_PASSWORD_BYTES", 1024, 0)), |
| 117 | CIDockerSocket: os.Getenv("CI_DOCKER_SOCKET"), |
| 118 | CIMaxHistory: int(intEnv("CI_MAX_HISTORY", 50, 1)), |
| 119 | CIDefaultTimeout: int(intEnv("CI_DEFAULT_TIMEOUT", 3600, 1)), |
| 120 | CIMaxConcurrent: int(intEnv("CI_MAX_CONCURRENT", 2, 1)), |
| 121 | CIMaxArtifactBytes: intEnv("CI_MAX_ARTIFACT_BYTES", 512<<20, 1), |
| 122 | MaxConcurrentArchives: int(intEnv("MAX_CONCURRENT_ARCHIVE_JOBS", 2, 1)), |
| 123 | } |
| 124 | switch c.RegistrationType { |
| 125 | case "enabled", "disabled", "queue": |
| 126 | default: |
| 127 | return nil, fmt.Errorf("REGISTRATION_TYPE %q must be enabled, disabled or queue", c.RegistrationType) |
| 128 | } |
| 129 | u, err := url.Parse(c.BaseURL) |
| 130 | if err != nil || u.Host == "" { |
| 131 | return nil, fmt.Errorf("BASE_URL %q is not a valid URL", c.BaseURL) |
| 132 | } |
| 133 | c.PublicHTTPS = u.Scheme == "https" |
| 134 | c.PublicOrigin = u.Scheme + "://" + u.Host |
| 135 | c.CommitterEmail = strEnv("COMMITTER_EMAIL", owner+"@"+u.Hostname()) |
| 136 | return c, nil |
| 137 | } |
| 138 | |
| 139 | // Derived paths under DataDir. |
| 140 | func (c *Config) DBPath() string { return filepath.Join(c.DataDir, "hearthforge.db") } |
| 141 | func (c *Config) ReposDir() string { return filepath.Join(c.DataDir, "repos") } |
| 142 | func (c *Config) AvatarsDir() string { return filepath.Join(c.DataDir, "avatars") } |
| 143 | func (c *Config) ReleasesDir() string { return filepath.Join(c.DataDir, "releases") } |
| 144 | func (c *Config) AllowedSignersPath() string { return filepath.Join(c.DataDir, "allowed_signers") } |
| 145 | func (c *Config) CIArtifactsDir() string { return filepath.Join(c.DataDir, "ci", "artifacts") } |
| 146 |