package config import ( "fmt" "os" "strconv" "time" ) type Postgres struct { Host string Port int User string Password string Database string SSLMode string } func (p Postgres) DSN() string { return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s", p.User, p.Password, p.Host, p.Port, p.Database, p.SSLMode) } type S3 struct { Endpoint string Region string AccessKey string SecretKey string Bucket string UsePathStyle bool PublicURL string PresignTTL time.Duration } type Auth struct { JWTSecret string AccessTTL time.Duration RefreshTTL time.Duration } type Server struct { Addr string BaseURL string } type SSE struct { HeartbeatInterval time.Duration ClientBuffer int } type Worker struct { Interval time.Duration Batch int PendingTTL time.Duration } type Config struct { Postgres Postgres S3 S3 Auth Auth Server Server SSE SSE Worker Worker } func Load() (*Config, error) { pgPort, err := strconv.Atoi(getEnv("PG_PORT", "5432")) if err != nil { return nil, fmt.Errorf("invalid PG_PORT: %w", err) } accessTTL, err := time.ParseDuration(getEnv("JWT_ACCESS_TTL", "15m")) if err != nil { return nil, fmt.Errorf("invalid JWT_ACCESS_TTL: %w", err) } refreshTTL, err := time.ParseDuration(getEnv("JWT_REFRESH_TTL", "720h")) if err != nil { return nil, fmt.Errorf("invalid JWT_REFRESH_TTL: %w", err) } presignTTL, err := time.ParseDuration(getEnv("S3_PRESIGN_TTL", "1h")) if err != nil { return nil, fmt.Errorf("invalid S3_PRESIGN_TTL: %w", err) } heartbeat, err := time.ParseDuration(getEnv("SSE_HEARTBEAT_INTERVAL", "25s")) if err != nil { return nil, fmt.Errorf("invalid SSE_HEARTBEAT_INTERVAL: %w", err) } bufSize, err := strconv.Atoi(getEnv("SSE_CLIENT_BUFFER", "64")) if err != nil { return nil, fmt.Errorf("invalid SSE_CLIENT_BUFFER: %w", err) } pathStyle, err := strconv.ParseBool(getEnv("S3_USE_PATH_STYLE", "true")) if err != nil { return nil, fmt.Errorf("invalid S3_USE_PATH_STYLE: %w", err) } workerInterval, err := time.ParseDuration(getEnv("WORKER_INTERVAL", "5s")) if err != nil { return nil, fmt.Errorf("invalid WORKER_INTERVAL: %w", err) } workerBatch, err := strconv.Atoi(getEnv("WORKER_BATCH", "10")) if err != nil { return nil, fmt.Errorf("invalid WORKER_BATCH: %w", err) } workerPendingTTL, err := time.ParseDuration(getEnv("WORKER_PENDING_TTL", "10m")) if err != nil { return nil, fmt.Errorf("invalid WORKER_PENDING_TTL: %w", err) } cfg := &Config{ Postgres: Postgres{ Host: getEnv("PG_HOST", "pg.localnet"), Port: pgPort, User: getEnv("PG_USER", "album"), Password: getEnv("PG_PASSWORD", ""), Database: getEnv("PG_DATABASE", "shared_album"), SSLMode: getEnv("PG_SSLMODE", "disable"), }, S3: S3{ Endpoint: getEnv("S3_ENDPOINT", "http://s3.localnet:3900"), Region: getEnv("S3_REGION", "garage"), AccessKey: getEnv("S3_ACCESS_KEY", ""), SecretKey: getEnv("S3_SECRET_KEY", ""), Bucket: getEnv("S3_BUCKET", "albums"), UsePathStyle: pathStyle, PublicURL: getEnv("S3_PUBLIC_URL", ""), PresignTTL: presignTTL, }, Auth: Auth{ JWTSecret: getEnv("JWT_SECRET", ""), AccessTTL: accessTTL, RefreshTTL: refreshTTL, }, Server: Server{ Addr: getEnv("HTTP_ADDR", ":8080"), BaseURL: getEnv("APP_BASE_URL", "http://localhost:8080"), }, SSE: SSE{ HeartbeatInterval: heartbeat, ClientBuffer: bufSize, }, Worker: Worker{ Interval: workerInterval, Batch: workerBatch, PendingTTL: workerPendingTTL, }, } if cfg.Auth.JWTSecret == "" { return nil, fmt.Errorf("JWT_SECRET is required") } if cfg.Postgres.Password == "" { return nil, fmt.Errorf("PG_PASSWORD is required") } return cfg, nil } func getEnv(key, fallback string) string { if v, ok := os.LookupEnv(key); ok && v != "" { return v } return fallback }