Files
sub-store/internal/config/config.go
T
rogee 3671c39b7c feat: single Docker image with frontend served from backend
- Dockerfile: 3-stage build (frontend npm build → backend go build → alpine runtime)
- Frontend dist files served from disk at /app/frontend/dist
- server.go: catch-all GET handler serves static files with SPA fallback to index.html
- config.go: new server.frontend_dir option (env: SUB_STORE_SERVER_FRONTEND_DIR)
- API routes (/api/*), download routes, and /health take priority over static serving
2026-07-28 14:52:53 +08:00

121 lines
4.0 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"`
}
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("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
}