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 } type Auth struct { 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 Storage struct { Backend string FSRoot string URLSecret string URLTTL time.Duration } type Database struct { Backend string SQLitePath string } type Config struct { Postgres Postgres S3 S3 Storage Storage Database Database 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) } urlTTL, err := time.ParseDuration(getEnv("OBJECT_URL_TTL", "1h")) if err != nil { return nil, fmt.Errorf("invalid OBJECT_URL_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, }, Storage: Storage{ Backend: getEnv("STORAGE_BACKEND", "fs"), FSRoot: getEnv("FS_ROOT", "./data"), URLSecret: getEnv("URL_SECRET", ""), URLTTL: urlTTL, }, Database: Database{ Backend: getEnv("DB_BACKEND", "sqlite"), SQLitePath: getEnv("SQLITE_PATH", "./galo.db"), }, Auth: Auth{ 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, }, } switch cfg.Database.Backend { case "postgres": if cfg.Postgres.Password == "" { return nil, fmt.Errorf("PG_PASSWORD is required") } case "sqlite": if cfg.Database.SQLitePath == "" { return nil, fmt.Errorf("SQLITE_PATH is required when DB_BACKEND=sqlite") } default: return nil, fmt.Errorf("invalid DB_BACKEND %q (want postgres or sqlite)", cfg.Database.Backend) } switch cfg.Storage.Backend { case "s3": case "fs": if cfg.Storage.FSRoot == "" { return nil, fmt.Errorf("FS_ROOT is required when STORAGE_BACKEND=fs") } default: return nil, fmt.Errorf("invalid STORAGE_BACKEND %q (want s3 or fs)", cfg.Storage.Backend) } return cfg, nil } func getEnv(key, fallback string) string { if v, ok := os.LookupEnv(key); ok && v != "" { return v } return fallback }