package config import ( "errors" "fmt" "os" "path/filepath" "github.com/BurntSushi/toml" ) type Config struct { Server ServerConfig `toml:"server"` Security SecurityConfig `toml:"security"` Apify ApifyConfig `toml:"apify"` Ntfy NtfyConfig `toml:"ntfy"` Scheduler SchedulerConfig `toml:"scheduler"` } type ServerConfig struct { Port int `toml:"port"` DBPath string `toml:"db_path"` } type SecurityConfig struct { SessionSecret string `toml:"session_secret"` EncryptionKey string `toml:"encryption_key"` } type ApifyConfig struct { APIKey string `toml:"api_key"` Actors ActorConfig `toml:"actors"` Proxy ProxyConfig `toml:"proxy"` } // ProxyConfig controls the proxyConfiguration block passed to apify actors // that scrape sites which block datacenter IPs (e.g. eBay returns 403 without // a residential proxy). type ProxyConfig struct { UseApifyProxy bool `toml:"use_apify_proxy"` Groups []string `toml:"groups"` Country string `toml:"country"` } type ActorConfig struct { ActiveListings string `toml:"active_listings"` SoldListings string `toml:"sold_listings"` PriceComparison string `toml:"price_comparison"` YahooAuctionsJP string `toml:"yahoo_auctions_jp"` YahooAuctionsJPSold string `toml:"yahoo_auctions_jp_sold"` MercariJP string `toml:"mercari_jp"` } type NtfyConfig struct { BaseURL string `toml:"base_url"` DefaultTopic string `toml:"default_topic"` } type SchedulerConfig struct { GlobalPollIntervalMinutes int `toml:"global_poll_interval_minutes"` MatchConfidenceThreshold float64 `toml:"match_confidence_threshold"` } func Load(path string) (*Config, error) { if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { return nil, fmt.Errorf("config file not found at %s. Copy config.toml.example to that path and fill it in", path) } else if err != nil { return nil, fmt.Errorf("stat config: %w", err) } var c Config if _, err := toml.DecodeFile(path, &c); err != nil { return nil, fmt.Errorf("parse config: %w", err) } if err := c.validate(); err != nil { return nil, err } return &c, nil } func (c *Config) validate() error { if len(c.Security.SessionSecret) < 32 { return errors.New("security.session_secret must be at least 32 bytes") } if len(c.Security.EncryptionKey) < 32 { return errors.New("security.encryption_key must be at least 32 bytes") } if c.Security.SessionSecret == c.Security.EncryptionKey { return errors.New("security.session_secret and security.encryption_key must not be equal") } if c.Server.DBPath == "" { return errors.New("server.db_path must be set") } dir := filepath.Dir(c.Server.DBPath) if dir == "" { dir = "." } if err := checkWritable(dir); err != nil { return fmt.Errorf("server.db_path directory %s not writable: %w", dir, err) } if c.Server.Port == 0 { c.Server.Port = 8080 } if c.Scheduler.GlobalPollIntervalMinutes == 0 { c.Scheduler.GlobalPollIntervalMinutes = 60 } if c.Scheduler.MatchConfidenceThreshold == 0 { c.Scheduler.MatchConfidenceThreshold = 0.6 } if c.Ntfy.DefaultTopic == "" { c.Ntfy.DefaultTopic = "veola" } return nil } func checkWritable(dir string) error { f, err := os.CreateTemp(dir, ".veola-write-test-*") if err != nil { return err } name := f.Name() f.Close() return os.Remove(name) }