Files
dygou_go/internal/config/config.go
T

228 lines
6.2 KiB
Go

package config
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"strconv"
"douyu_go/internal/activity"
"douyu_go/internal/constants"
imgpkg "douyu_go/internal/image"
"douyu_go/internal/logger"
"douyu_go/internal/types"
)
// DefaultAppPaths 收集运行时路径,支持环境变量覆盖。
// 默认数据存放在当前用户的应用配置目录,而非工作目录,避免 macOS 应用包目录无写权限。
func DefaultAppPaths() types.AppPaths {
dataDir := resolvePath(envOrDefault("DOUYU_DATA_DIR", defaultAppDataDir()))
configFile := filepath.Join(dataDir, constants.DefaultConfigFile)
accountFile := filepath.Join(dataDir, constants.DefaultAccountFile)
legacyConfigFile := resolvePath(constants.LegacyDefaultConfigFile)
legacyAccountFile := resolvePath(constants.LegacyDefaultAccountFile)
if value := os.Getenv("DOUYU_CONFIG"); value != "" {
configFile = resolvePath(value)
legacyConfigFile = ""
}
if value := os.Getenv("DOUYU_USERS"); value != "" {
accountFile = resolvePath(value)
legacyAccountFile = ""
}
return types.AppPaths{
DataDir: dataDir,
ConfigFile: configFile,
AccountFile: accountFile,
LegacyConfigFile: legacyConfigFile,
LegacyAccountFile: legacyAccountFile,
LogDir: resolvePath(envOrDefault("DOUYU_LOG_DIR", filepath.Join(dataDir, constants.DefaultLogDir))),
SuccessDir: resolvePath(envOrDefault("DOUYU_SUCCESS_DIR", filepath.Join(dataDir, constants.DefaultSuccessDir))),
FontFiles: imgpkg.CollectFontCandidates(),
}
}
func defaultAppDataDir() string {
if dir, err := os.UserConfigDir(); err == nil && dir != "" {
return filepath.Join(dir, constants.DefaultAppDataDirName)
}
if home, err := os.UserHomeDir(); err == nil && home != "" {
return filepath.Join(home, "."+constants.DefaultAppDataDirName)
}
return "." + constants.DefaultAppDataDirName
}
// resolvePath 将相对路径解析为绝对路径。
func resolvePath(path string) string {
if filepath.IsAbs(path) {
return path
}
if abs, err := filepath.Abs(path); err == nil {
return abs
}
return path
}
func envOrDefault(key, fallback string) string {
if value := os.Getenv(key); value != "" {
return filepath.Clean(value)
}
return fallback
}
// MigrateLegacyFile 在目标文件不存在时复制旧版本数据,不删除旧文件。
// 返回值表示是否完成了迁移。
func MigrateLegacyFile(target, legacy string) (bool, error) {
if target == "" || legacy == "" || samePath(target, legacy) {
return false, nil
}
if _, err := os.Stat(target); err == nil {
return false, nil
} else if !errors.Is(err, os.ErrNotExist) {
return false, err
}
content, err := os.ReadFile(legacy)
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
if err != nil {
return false, err
}
if err := os.MkdirAll(filepath.Dir(target), 0700); err != nil {
return false, err
}
if err := os.WriteFile(target, content, 0600); err != nil {
return false, err
}
return true, nil
}
func samePath(left, right string) bool {
return filepath.Clean(left) == filepath.Clean(right)
}
// LoadTraceConfig 读取 trace 开关;默认关闭,避免保存敏感调试数据。
func LoadTraceConfig() types.TraceConfig {
return types.TraceConfig{
Enabled: isTruthyEnv("DOUYU_TRACE"),
MaxBodyBytes: envInt("DOUYU_TRACE_MAX_BODY", 0),
}
}
func isTruthyEnv(key string) bool {
switch os.Getenv(key) {
case "1", "true", "TRUE", "yes", "YES", "on", "ON":
return true
default:
return false
}
}
func envInt(key string, fallback int) int {
raw := os.Getenv(key)
if raw == "" {
return fallback
}
value, err := strconv.Atoi(raw)
if err != nil || value <= 0 {
return fallback
}
return value
}
// defaultConfig 返回内置默认配置。
func defaultConfig() types.Config {
return types.Config{
Activity: activity.DefaultActivityID,
TimeAPIURL: constants.DefaultTimeAPI,
TraceEnabled: false,
TraceMaxBody: 0,
LogLevel: "info",
Concurrency: constants.DefaultConcurrency,
Attempts: constants.DefaultAttempts,
RetryStages: defaultRetryStages(),
SeckillMode: types.SeckillModeAggressive,
XiaodianPageSize: 200,
}
}
// defaultRetryStages 返回默认重试间隔阶段配置。
func defaultRetryStages() []types.RetryStage {
return []types.RetryStage{
{MaxElapsed: 4, Interval: 120},
{MaxElapsed: 12, Interval: 200},
{MaxElapsed: 30, Interval: 500},
{MaxElapsed: 75, Interval: 1000},
}
}
// LoadConfig 读取配置文件;缺失时安静使用内置默认配置。
func LoadConfig(path string) types.Config {
cfg := defaultConfig()
if path == "" {
path = constants.DefaultConfigFile
}
f, err := os.Open(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
logger.LogEvent("配置", "info", "配置加载完成: 内置默认配置")
} else {
logger.LogEvent("配置", "warn", "读取 %s 出错,使用内置默认配置: %v", path, err)
}
return cfg
}
defer f.Close()
if err := json.NewDecoder(f).Decode(&cfg); err != nil {
logger.LogEvent("配置", "warn", "解析 %s 失败,使用内置默认配置: %v", path, err)
return defaultConfig()
}
if cfg.Activity == "" {
cfg.Activity = activity.DefaultActivityID
}
if cfg.TimeAPIURL == "" {
cfg.TimeAPIURL = constants.DefaultTimeAPI
}
if cfg.Concurrency <= 0 {
cfg.Concurrency = constants.DefaultConcurrency
}
if cfg.Attempts <= 0 {
cfg.Attempts = constants.DefaultAttempts
}
if cfg.LogLevel == "" {
cfg.LogLevel = "info"
}
if len(cfg.RetryStages) == 0 {
cfg.RetryStages = defaultRetryStages()
}
normalizeXiaodianConfig(&cfg)
logger.LogEvent("配置", "success", "配置加载完成: %s", path)
return cfg
}
// normalizeXiaodianConfig 约束和平小店分页参数,接口单页最大为 200。
func normalizeXiaodianConfig(cfg *types.Config) {
if cfg.XiaodianPageSize <= 0 || cfg.XiaodianPageSize > 200 {
cfg.XiaodianPageSize = 200
}
}
// SaveConfig 将配置写入文件。
func SaveConfig(path string, cfg types.Config) error {
if path == "" {
path = constants.DefaultConfigFile
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
return os.WriteFile(path, data, 0600)
}