1. sslinks 403: Cloudflare blocks server IP. Add fetcher.proxy_url config so subscription/flow requests can route through an HTTP proxy. 2. Egress probe: Docker image lacked sing-box. Install sing-box v1.11.4 in the Alpine runtime stage. 3. Collection rename: group by display baseName instead of internal groupKey, format index as -N instead of %02d. Fixes double-suffix issue where EnsureUniqueProxyNames appended -2/-3 onto existing 02.
123 lines
4.1 KiB
Go
123 lines
4.1 KiB
Go
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
|
|
}
|