updates
This commit is contained in:
208
server/internal/config/config.go
Normal file
208
server/internal/config/config.go
Normal file
@@ -0,0 +1,208 @@
|
||||
// internal/config/config.go
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
Database DatabaseConfig
|
||||
Redis RedisConfig
|
||||
SMTP SMTPConfig
|
||||
JWT JWTConfig
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port string
|
||||
Env string
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Password string
|
||||
Name string
|
||||
SSLMode string
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Addr string
|
||||
Password string
|
||||
DB int
|
||||
}
|
||||
|
||||
type SMTPConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
From string
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
Secret string
|
||||
PublicKey string
|
||||
Expiry string
|
||||
}
|
||||
|
||||
// Package-level state for a single, watched config instance
|
||||
var (
|
||||
vip *viper.Viper
|
||||
cfgCache *Config
|
||||
cfgMu sync.RWMutex
|
||||
initOnce sync.Once
|
||||
initErr error
|
||||
|
||||
listeners []func(*Config)
|
||||
listenersMu sync.RWMutex
|
||||
)
|
||||
|
||||
// LoadConfig returns the current configuration. On first call it initializes
|
||||
// Viper, loads the config from a deterministic location, and starts watching
|
||||
// for changes. Subsequent calls are cheap and thread-safe.
|
||||
func LoadConfig() (*Config, error) {
|
||||
initOnce.Do(func() {
|
||||
vip = viper.New()
|
||||
configureViper(vip)
|
||||
// Initial read must succeed; later changes are handled via WatchConfig
|
||||
if err := vip.ReadInConfig(); err != nil {
|
||||
initErr = fmt.Errorf("failed to load config: %w", err)
|
||||
return
|
||||
}
|
||||
// Build cache from initial values
|
||||
cfg := buildFromViper(vip)
|
||||
cfgMu.Lock()
|
||||
cfgCache = cfg
|
||||
cfgMu.Unlock()
|
||||
|
||||
// Watch for changes and update cache atomically
|
||||
vip.WatchConfig()
|
||||
vip.OnConfigChange(func(_ fsnotify.Event) {
|
||||
// viper already re-reads on change; rebuild view and swap
|
||||
newCfg := buildFromViper(vip)
|
||||
cfgMu.Lock()
|
||||
cfgCache = newCfg
|
||||
cfgMu.Unlock()
|
||||
notifyListeners(newCfg)
|
||||
})
|
||||
})
|
||||
|
||||
if initErr != nil {
|
||||
return nil, initErr
|
||||
}
|
||||
|
||||
cfgMu.RLock()
|
||||
defer cfgMu.RUnlock()
|
||||
return cloneConfig(cfgCache), nil
|
||||
}
|
||||
|
||||
// DSN returns the Data Source Name for PostgreSQL connection
|
||||
func (db DatabaseConfig) DSN() string {
|
||||
return fmt.Sprintf(
|
||||
"postgres://%s:%s@%s:%s/%s?sslmode=%s",
|
||||
db.User, db.Password, db.Host, db.Port, db.Name, db.SSLMode,
|
||||
)
|
||||
}
|
||||
|
||||
// RegisterChangeListener registers a callback invoked whenever the configuration
|
||||
// file changes and the in-memory configuration is updated.
|
||||
func RegisterChangeListener(listener func(*Config)) {
|
||||
listenersMu.Lock()
|
||||
defer listenersMu.Unlock()
|
||||
listeners = append(listeners, listener)
|
||||
}
|
||||
|
||||
// --- internal helpers ---
|
||||
|
||||
func configureViper(v *viper.Viper) {
|
||||
// Allow env vars to override any key (expects exact key names used below)
|
||||
v.AutomaticEnv()
|
||||
|
||||
if path, ok := resolveConfigFilePath(); ok {
|
||||
v.SetConfigFile(path)
|
||||
return
|
||||
}
|
||||
// Fallback to looking for a .env in stable, nearby locations.
|
||||
v.SetConfigName(".env")
|
||||
v.SetConfigType("env")
|
||||
v.AddConfigPath(".")
|
||||
v.AddConfigPath("..")
|
||||
}
|
||||
|
||||
func resolveConfigFilePath() (string, bool) {
|
||||
if p := os.Getenv("CONFIG_FILE"); p != "" {
|
||||
return p, true
|
||||
}
|
||||
cwd, _ := os.Getwd()
|
||||
candidates := []string{
|
||||
filepath.Join(cwd, ".env"),
|
||||
filepath.Join(cwd, "go-server", ".env"),
|
||||
filepath.Join(cwd, "..", ".env"),
|
||||
filepath.Join(cwd, "..", "go-server", ".env"),
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if info, err := os.Stat(c); err == nil && !info.IsDir() {
|
||||
return c, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func buildFromViper(v *viper.Viper) *Config {
|
||||
return &Config{
|
||||
Server: ServerConfig{
|
||||
Port: v.GetString("SERVER_PORT"),
|
||||
Env: v.GetString("SERVER_ENV"),
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Host: v.GetString("DB_HOST"),
|
||||
Port: v.GetString("DB_PORT"),
|
||||
User: v.GetString("DB_USER"),
|
||||
Password: v.GetString("DB_PASSWORD"),
|
||||
Name: v.GetString("DB_NAME"),
|
||||
SSLMode: v.GetString("DB_SSL_MODE"),
|
||||
},
|
||||
Redis: RedisConfig{
|
||||
Addr: v.GetString("REDIS_ADDR"),
|
||||
Password: v.GetString("REDIS_PASSWORD"),
|
||||
DB: v.GetInt("REDIS_DB"),
|
||||
},
|
||||
SMTP: SMTPConfig{
|
||||
Host: v.GetString("SMTP_HOST"),
|
||||
Port: v.GetInt("SMTP_PORT"),
|
||||
Username: v.GetString("SMTP_USERNAME"),
|
||||
Password: v.GetString("SMTP_PASSWORD"),
|
||||
From: v.GetString("SMTP_FROM"),
|
||||
},
|
||||
JWT: JWTConfig{
|
||||
Secret: v.GetString("JWT_SECRET"),
|
||||
Expiry: v.GetString("JWT_EXPIRY"),
|
||||
PublicKey: v.GetString("JWT_PUBLIC_KEY"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func cloneConfig(c *Config) *Config {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
cc := *c
|
||||
return &cc
|
||||
}
|
||||
|
||||
func notifyListeners(c *Config) {
|
||||
listenersMu.RLock()
|
||||
defer listenersMu.RUnlock()
|
||||
for _, l := range listeners {
|
||||
// Call listeners in-goroutine to avoid blocking the watcher
|
||||
go l(cloneConfig(c))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user