package config import ( "encoding/json" "fmt" "os" "path/filepath" "strconv" ) type Config struct { Server ServerConfig `json:"server"` Database DatabaseConfig `json:"database"` Content ContentConfig `json:"content"` Auth AuthConfig `json:"auth"` Email EmailConfig `json:"email"` LogLevel string `json:"logLevel"` DevMode bool `json:"devMode"` } type ServerConfig struct { Addr string `json:"addr"` } type DatabaseConfig struct { Path string `json:"path"` PrimaryURL string `json:"primaryUrl"` AuthToken string `json:"authToken"` } type ContentConfig struct { SourceDir string `json:"sourceDir"` StoreDir string `json:"storeDir"` } type AuthConfig struct { PublicOrigin string `json:"publicOrigin"` } type EmailConfig struct { SMTPHost string `json:"smtpHost"` SMTPPort int `json:"smtpPort"` From string `json:"from"` } func Default() Config { return Config{ Server: ServerConfig{ Addr: ":8080", }, Database: DatabaseConfig{ Path: filepath.Join("..", "..", "data", "db.sqlite"), }, Content: ContentConfig{ SourceDir: filepath.Join("..", "..", "content"), StoreDir: filepath.Join("..", "..", "data", "files"), }, Auth: AuthConfig{ PublicOrigin: "http://localhost:8080", }, Email: EmailConfig{ SMTPHost: "localhost", SMTPPort: 1025, From: "notifications@cairnquire.local", }, LogLevel: "INFO", } } func Load() (Config, error) { cfg := Default() if configPath := os.Getenv("CAIRNQUIRE_CONFIG"); configPath != "" { if err := loadFile(configPath, &cfg); err != nil { return Config{}, err } } overrideString(&cfg.Server.Addr, "CAIRNQUIRE_SERVER_ADDR") if port := os.Getenv("PORT"); port != "" { cfg.Server.Addr = ":" + port } overrideString(&cfg.Database.Path, "CAIRNQUIRE_DATABASE_PATH") overrideString(&cfg.Database.PrimaryURL, "CAIRNQUIRE_DATABASE_PRIMARY_URL") overrideString(&cfg.Database.AuthToken, "CAIRNQUIRE_DATABASE_AUTH_TOKEN") overrideString(&cfg.Content.SourceDir, "CAIRNQUIRE_CONTENT_SOURCE_DIR") overrideString(&cfg.Content.StoreDir, "CAIRNQUIRE_CONTENT_STORE_DIR") overrideString(&cfg.Auth.PublicOrigin, "CAIRNQUIRE_PUBLIC_ORIGIN") overrideString(&cfg.Email.SMTPHost, "CAIRNQUIRE_EMAIL_SMTP_HOST") if port := os.Getenv("CAIRNQUIRE_EMAIL_SMTP_PORT"); port != "" { if p, err := strconv.Atoi(port); err == nil { cfg.Email.SMTPPort = p } } overrideString(&cfg.Email.From, "CAIRNQUIRE_EMAIL_FROM") overrideString(&cfg.LogLevel, "CAIRNQUIRE_LOG_LEVEL") if devMode := os.Getenv("CAIRNQUIRE_DEV_MODE"); devMode != "" { parsed, err := strconv.ParseBool(devMode) if err != nil { return Config{}, fmt.Errorf("parse CAIRNQUIRE_DEV_MODE: %w", err) } cfg.DevMode = parsed } if cfg.Server.Addr == "" { return Config{}, fmt.Errorf("server.addr must not be empty") } return cfg, nil } func loadFile(path string, cfg *Config) error { file, err := os.Open(path) if err != nil { return fmt.Errorf("open config file %q: %w", path, err) } defer file.Close() if err := json.NewDecoder(file).Decode(cfg); err != nil { return fmt.Errorf("decode config file %q: %w", path, err) } return nil } func overrideString(target *string, key string) { if value := os.Getenv(key); value != "" { *target = value } }