package config import ( "fmt" "time" "github.com/spf13/viper" ) type Config struct { Server ServerConfig `mapstructure:"server"` Database DatabaseConfig `mapstructure:"database"` Auth AuthConfig `mapstructure:"auth"` Fetcher FetcherConfig `mapstructure:"fetcher"` Recycle RecycleConfig `mapstructure:"recycle"` App AppConfig `mapstructure:"app"` } type ServerConfig struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` ReadTimeout time.Duration `mapstructure:"read_timeout"` WriteTimeout time.Duration `mapstructure:"write_timeout"` BodyLimit int `mapstructure:"body_limit"` FrontendDir string `mapstructure:"frontend_dir"` } type DatabaseConfig struct { Path string `mapstructure:"path"` } type AuthConfig struct { AdminToken string `mapstructure:"admin_token"` DownloadToken string `mapstructure:"download_token"` DownloadHosts []string `mapstructure:"download_hosts"` } type FetcherConfig struct { DefaultTimeout time.Duration `mapstructure:"default_timeout"` DefaultUserAgent string `mapstructure:"default_user_agent"` DefaultFlowUA string `mapstructure:"default_flow_user_agent"` Concurrency int `mapstructure:"concurrency"` ConcurrencyWait time.Duration `mapstructure:"concurrency_wait"` CacheTTL time.Duration `mapstructure:"cache_ttl"` CacheStaleOnError bool `mapstructure:"cache_stale_on_error"` MaxSourceUrls int `mapstructure:"max_source_urls"` MaxResponseBytes int `mapstructure:"max_response_bytes"` MaxTotalBytes int `mapstructure:"max_total_bytes"` ProxyURL string `mapstructure:"proxy_url"` } type RecycleConfig struct { MaxEntries int `mapstructure:"max_entries"` } type AppConfig struct { Name string `mapstructure:"name"` Version string `mapstructure:"version"` } func defaults() { viper.SetDefault("server.host", "0.0.0.0") viper.SetDefault("server.port", 3000) viper.SetDefault("server.read_timeout", 30*time.Second) viper.SetDefault("server.write_timeout", 60*time.Second) viper.SetDefault("server.body_limit", 4*1024*1024) viper.SetDefault("server.frontend_dir", "./frontend/dist") viper.SetDefault("database.path", "./data/sub-store.db") viper.SetDefault("auth.admin_token", "") viper.SetDefault("auth.download_token", "") viper.SetDefault("auth.download_hosts", []string{}) viper.SetDefault("fetcher.default_timeout", 30*time.Second) viper.SetDefault("fetcher.default_user_agent", "clash.meta/v1.19.24") viper.SetDefault("fetcher.default_flow_user_agent", "clash.meta/v1.19.24") viper.SetDefault("fetcher.concurrency", 3) viper.SetDefault("fetcher.concurrency_wait", 0*time.Second) viper.SetDefault("fetcher.cache_ttl", 300*time.Second) viper.SetDefault("fetcher.cache_stale_on_error", true) viper.SetDefault("fetcher.max_source_urls", 8) viper.SetDefault("fetcher.max_response_bytes", 2*1024*1024) viper.SetDefault("fetcher.max_total_bytes", 12*1024*1024) viper.SetDefault("fetcher.proxy_url", "") viper.SetDefault("recycle.max_entries", 50) viper.SetDefault("app.name", "Sub-Store") viper.SetDefault("app.version", "1.0.0") } func Load(configPath string) (*Config, error) { defaults() if configPath != "" { viper.SetConfigFile(configPath) } else { viper.SetConfigName("config") viper.SetConfigType("yaml") viper.AddConfigPath(".") viper.AddConfigPath("./config") } viper.SetEnvPrefix("SUB_STORE") viper.AutomaticEnv() if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { return nil, fmt.Errorf("failed to read config: %w", err) } } var cfg Config if err := viper.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("failed to unmarshal config: %w", err) } if cfg.Auth.AdminToken == "" { return nil, fmt.Errorf("auth.admin_token is required (set in config.yaml or SUB_STORE_AUTH_ADMIN_TOKEN env)") } if cfg.Auth.DownloadToken == "" { return nil, fmt.Errorf("auth.download_token is required (set in config.yaml or SUB_STORE_AUTH_DOWNLOAD_TOKEN env)") } return &cfg, nil }