123
internal/config/config.go
Normal file
123
internal/config/config.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Config holds all configuration for the Zwift monitor service
|
||||
type Config struct {
|
||||
// Authentication
|
||||
Username string `mapstructure:"username"`
|
||||
Password string `mapstructure:"password"`
|
||||
|
||||
// Storage
|
||||
OutputDir string `mapstructure:"output_dir"`
|
||||
StateFile string `mapstructure:"state_file"`
|
||||
TokenCachePath string `mapstructure:"token_cache"`
|
||||
|
||||
// Monitoring
|
||||
PollInterval time.Duration `mapstructure:"poll_interval"`
|
||||
|
||||
// API Configuration
|
||||
RateLimit int `mapstructure:"rate_limit"`
|
||||
MaxRetries int `mapstructure:"max_retries"`
|
||||
|
||||
// Logging
|
||||
LogLevel string `mapstructure:"log_level"`
|
||||
LogFile string `mapstructure:"log_file"`
|
||||
}
|
||||
|
||||
// Load loads configuration from environment variables and optional config file
|
||||
func Load() (*Config, error) {
|
||||
v := viper.New()
|
||||
|
||||
// Set defaults
|
||||
v.SetDefault("output_dir", "/data/activities")
|
||||
v.SetDefault("state_file", "/data/zwift-state.json")
|
||||
v.SetDefault("token_cache", "/data/.zwift-token.json")
|
||||
v.SetDefault("poll_interval", "5m")
|
||||
v.SetDefault("rate_limit", 5)
|
||||
v.SetDefault("max_retries", 3)
|
||||
v.SetDefault("log_level", "info")
|
||||
v.SetDefault("log_file", "")
|
||||
|
||||
// Bind environment variables
|
||||
v.SetEnvPrefix("ZWIFT")
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
v.AutomaticEnv()
|
||||
|
||||
// Try to load config file if it exists
|
||||
configPaths := []string{
|
||||
"config.yaml",
|
||||
"/app/config.yaml",
|
||||
filepath.Join(os.Getenv("HOME"), ".zwift-monitor.yaml"),
|
||||
}
|
||||
|
||||
for _, path := range configPaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
v.SetConfigFile(path)
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file %s: %w", path, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := v.Unmarshal(&cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Expand paths (handle ~)
|
||||
cfg.OutputDir = expandPath(cfg.OutputDir)
|
||||
cfg.StateFile = expandPath(cfg.StateFile)
|
||||
cfg.TokenCachePath = expandPath(cfg.TokenCachePath)
|
||||
if cfg.LogFile != "" {
|
||||
cfg.LogFile = expandPath(cfg.LogFile)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// Validate checks if required configuration is present
|
||||
func (c *Config) Validate() error {
|
||||
if c.Username == "" {
|
||||
return fmt.Errorf("username is required (set ZWIFT_USERNAME environment variable)")
|
||||
}
|
||||
if c.Password == "" {
|
||||
return fmt.Errorf("password is required (set ZWIFT_PASSWORD environment variable)")
|
||||
}
|
||||
if c.PollInterval < time.Minute {
|
||||
return fmt.Errorf("poll_interval must be at least 1 minute")
|
||||
}
|
||||
if c.RateLimit < 1 {
|
||||
return fmt.Errorf("rate_limit must be at least 1")
|
||||
}
|
||||
if c.MaxRetries < 1 {
|
||||
return fmt.Errorf("max_retries must be at least 1")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// expandPath expands ~ to home directory
|
||||
func expandPath(path string) string {
|
||||
if strings.HasPrefix(path, "~") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return path
|
||||
}
|
||||
return filepath.Join(home, path[1:])
|
||||
}
|
||||
return path
|
||||
}
|
||||
Reference in New Issue
Block a user