package config import ( "fmt" "os" "path/filepath" "runtime" "github.com/BurntSushi/toml" ) // DatabaseConfig holds database connection settings. type DatabaseConfig struct { Driver string `toml:"driver"` DSN string `toml:"dsn"` } // WebConfig holds web server settings. type WebConfig struct { // Addr 监听地址:以 / 开头视为 unix socket,否则视为 TCP 端口 Addr string `toml:"addr"` } // SpeedtestConfig holds speed test limits. type SpeedtestConfig struct { MaxDownloadBytes int64 `toml:"max_download_bytes"` // 单次下载请求最大字节数 MaxUploadBytes int64 `toml:"max_upload_bytes"` // 单次上传请求最大字节数 } // Config is the top-level configuration structure. type Config struct { Database DatabaseConfig `toml:"database"` Web WebConfig `toml:"web"` Speedtest SpeedtestConfig `toml:"speedtest"` } func isWindows() bool { return runtime.GOOS == "windows" } func etcDir() string { if isWindows() { return WinEtcDir } return LinuxEtcDir } func baseDir() string { if isWindows() { return WinBaseDir } return LinuxBaseDir } func defaultDSN() string { if isWindows() { return DefaultDSNWin } return DefaultDSNLinux } // defaultConfig returns a fully populated Config with default values. func defaultConfig() *Config { return &Config{ Database: DatabaseConfig{ Driver: DefaultDBDriver, DSN: defaultDSN(), }, Web: WebConfig{ Addr: DefaultWebAddr, }, Speedtest: SpeedtestConfig{ MaxDownloadBytes: DefaultMaxDownloadBytes, MaxUploadBytes: DefaultMaxUploadBytes, }, } } // configFilePath returns the full path to the configuration file. // 支持通过环境变量 SPEEDTEST_CONFIG 覆盖(本地调试用)。 func configFilePath() string { if p := os.Getenv("SPEEDTEST_CONFIG"); p != "" { return p } return filepath.Join(etcDir(), ConfigFileName) } // mergeDefaults overlays default values onto the loaded config for any zero/empty fields. func mergeDefaults(cfg *Config, defaults *Config) *Config { if cfg.Database.Driver == "" { cfg.Database.Driver = defaults.Database.Driver } if cfg.Database.DSN == "" { cfg.Database.DSN = defaults.Database.DSN } if cfg.Web.Addr == "" { cfg.Web.Addr = defaults.Web.Addr } if cfg.Speedtest.MaxDownloadBytes == 0 { cfg.Speedtest.MaxDownloadBytes = defaults.Speedtest.MaxDownloadBytes } if cfg.Speedtest.MaxUploadBytes == 0 { cfg.Speedtest.MaxUploadBytes = defaults.Speedtest.MaxUploadBytes } return cfg } // writeConfig writes the configuration to the given file path. func writeConfig(path string, cfg *Config) error { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("创建配置目录失败 %s: %w", dir, err) } f, err := os.Create(path) if err != nil { return fmt.Errorf("创建配置文件失败 %s: %w", path, err) } defer f.Close() enc := toml.NewEncoder(f) if err := enc.Encode(cfg); err != nil { return fmt.Errorf("写入配置文件失败: %w", err) } return nil } // LoadConfig loads the configuration from disk. // If the configuration file does not exist, it creates one with default values. // If the file exists but has missing fields, they are filled with defaults and the file is updated. func LoadConfig() (*Config, error) { path := configFilePath() defaults := defaultConfig() if _, err := os.Stat(path); os.IsNotExist(err) { if mkErr := writeConfig(path, defaults); mkErr != nil { return nil, mkErr } return defaults, nil } data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("读取配置文件失败 %s: %w", path, err) } cfg := &Config{} if err := toml.Unmarshal(data, cfg); err != nil { return nil, fmt.Errorf("解析配置文件失败: %w", err) } merged := mergeDefaults(cfg, defaults) if writeErr := writeConfig(path, merged); writeErr != nil { return nil, writeErr } return merged, nil }