Files
zwift-activity-loader/internal/config/config.go
Thomas Klaehn f496eebe2a Initial commit
Signed-off-by: Thomas Klaehn <thomas.klaehn@perinet.io>
2026-02-10 13:27:42 +01:00

124 lines
3.0 KiB
Go

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
}