976f5238f8
- internal .env loader (real env wins) + strictly validated config struct - ULID package: 48-bit ms timestamp + 80-bit crypto randomness, sortable - HTTP server with recovery/request-id/trusted-proxy/access-log middleware - /healthz, /readyz (pluggable checkers), /metricsz (counter registry) - multi-stage Dockerfile (alpine, non-root) + compose with healthchecks, app bound to 127.0.0.1:8787, mongo unpublished, graceful 15s shutdown Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
51 lines
1.3 KiB
Go
51 lines
1.3 KiB
Go
package config
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// LoadDotEnv reads a dotenv file and sets each variable into the process
|
|
// environment unless it is already set (real environment wins). Supported
|
|
// syntax: KEY=VALUE, blank lines, full-line and unquoted inline comments,
|
|
// optional `export ` prefix, single- or double-quoted values.
|
|
func LoadDotEnv(path string) error {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("open %s: %w", path, err)
|
|
}
|
|
defer f.Close()
|
|
|
|
sc := bufio.NewScanner(f)
|
|
for n := 1; sc.Scan(); n++ {
|
|
line := strings.TrimSpace(sc.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
line = strings.TrimPrefix(line, "export ")
|
|
key, val, ok := strings.Cut(line, "=")
|
|
if !ok || strings.TrimSpace(key) == "" {
|
|
return fmt.Errorf("%s:%d: not a KEY=VALUE line", path, n)
|
|
}
|
|
key = strings.TrimSpace(key)
|
|
val = strings.TrimSpace(val)
|
|
switch {
|
|
case len(val) >= 2 && (val[0] == '"' || val[0] == '\'') && val[len(val)-1] == val[0]:
|
|
val = val[1 : len(val)-1]
|
|
default: // unquoted: strip trailing inline comment
|
|
if i := strings.Index(val, " #"); i >= 0 {
|
|
val = strings.TrimSpace(val[:i])
|
|
}
|
|
}
|
|
if _, exists := os.LookupEnv(key); !exists {
|
|
os.Setenv(key, val)
|
|
}
|
|
}
|
|
return sc.Err()
|
|
}
|