Files
sub-store/internal/config/config.go
T
rogee 040e3ebab1 fix: fetcher proxy support, Dockerfile sing-box, rename index format
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.
2026-07-28 17:38:57 +08:00

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
}