Files
go_blog/config/config.go
T
kevin f307781f58 docs: 全部 Go 代码注释汉化
- 48 个 Go 文件所有注释(行注释/块注释/行尾注释,含 _test.go)翻译为中文
- 保留技术标识符:SECURITY_TODO(n)、unsafe-inline、sqlite/mysql、路由参数等
- 代码、字符串字面量、日志消息保持英文原文,零逻辑改动
- go build/vet 通过,go test -count=1 ./... 全绿
2026-08-27 19:03:03 +08:00

174 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"crypto/rand"
"encoding/hex"
"log"
"os"
"path/filepath"
"runtime"
"gopkg.in/yaml.v3"
)
// Config 保存全部应用配置。
type Config struct {
Database DatabaseConfig `yaml:"database"`
Web WebConfig `yaml:"web"`
Path string `yaml:"path"`
Secret string `yaml:"secret"`
}
// DatabaseConfig 保存数据库相关配置。
type DatabaseConfig struct {
Type string `yaml:"type"` // "sqlite"(默认)或 "mysql"
DSN string `yaml:"dsn"` // MySQL 连接字符串(type 为 "mysql" 时必填)
}
// WebConfig 保存 Web 服务器监听配置。
type WebConfig struct {
Port string `yaml:"port"` // TCP 端口,"" 或 "0" 表示禁用
Socket string `yaml:"socket"` // Unix Socket 路径,"" 表示禁用
// TrustedProxies 列出可信代理 IP/CIDR,这些代理的 X-Forwarded-For /
// X-Forwarded-Proto 请求头将被信任(例如位于应用前方的 Caddy/nginx
// 服务器)。默认为回环地址。如果应用直接暴露给客户端,请保持默认值,
// 以免客户端伪造 X-Forwarded-For 欺骗记录中的 IP。
TrustedProxies []string `yaml:"trusted_proxies"`
}
// defaultTrustedProxies 在配置中省略 trusted_proxies 时使用。
var defaultTrustedProxies = []string{"127.0.0.1", "::1"}
const defaultPort = "8080"
// mysqlExampleDSN 会写入新建的配置文件,作为参考示例。
const mysqlExampleDSN = "user:password@tcp(127.0.0.1:3306)/blog_go?charset=utf8mb4&parseTime=True&loc=Local"
// getConfigPath 返回按操作系统区分的配置目录和配置文件路径。
func getConfigPath() (dir, file string) {
switch runtime.GOOS {
case "windows":
dir = filepath.Join(".", "win", "etc", "blog_go")
case "darwin":
dir = filepath.Join(".", "mac", "etc", "blog_go")
default:
dir = filepath.Join("/", "etc", "blog_go")
}
file = filepath.Join(dir, "config.yaml")
return
}
// getDefaultStoragePath 返回按操作系统区分的默认存储路径。
func getDefaultStoragePath() string {
switch runtime.GOOS {
case "windows":
return filepath.Join(".", "win", "srv", "blog_go")
case "darwin":
return filepath.Join(".", "mac", "srv", "blog_go")
default:
return filepath.Join("/", "srv", "blog_go")
}
}
// generateSecret 为会话密钥生成密码学随机的十六进制字符串。
// crypto/rand 的失败无法恢复,因此程序将直接终止,而不会退回到可预测的值。
func generateSecret() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
log.Fatalf("Failed to generate session secret: %v", err)
}
return hex.EncodeToString(b)
}
// getDefaultSocketPath 返回按操作系统区分的默认 Unix Socket 路径。
func getDefaultSocketPath() string {
if runtime.GOOS == "linux" {
return "/run/blog_go/web.sock"
}
return ""
}
// LoadConfig 读取配置文件;若不存在则按默认值创建。
// 若 customPath 非空,则覆盖按操作系统区分的配置文件路径。
func LoadConfig(customPath string) *Config {
configDir, configFile := getConfigPath()
if customPath != "" {
configFile = customPath
configDir = filepath.Dir(configFile)
}
defaultPath := getDefaultStoragePath()
// 检查配置文件是否存在;不存在则按默认值创建。
if _, err := os.Stat(configFile); os.IsNotExist(err) {
log.Printf("Config file not found at %s, creating with defaults...", configFile)
if err := os.MkdirAll(configDir, 0755); err != nil {
log.Fatalf("Failed to create config directory %s: %v", configDir, err)
}
cfg := &Config{
Database: DatabaseConfig{
Type: "sqlite",
DSN: mysqlExampleDSN,
},
Web: WebConfig{
Port: defaultPort,
Socket: getDefaultSocketPath(),
},
Path: defaultPath,
Secret: generateSecret(),
}
data, err := yaml.Marshal(cfg)
if err != nil {
log.Fatalf("Failed to marshal default config: %v", err)
}
if err := os.WriteFile(configFile, data, 0640); err != nil {
log.Fatalf("Failed to write config file %s: %v", configFile, err)
}
// SECURITY_TODO #11:配置文件保存会话密钥;仅允许所有者读取
// install_linux.sh 已应用 0640 权限)。
log.Printf("Default config created at %s", configFile)
return cfg
}
// 读取现有配置文件。
data, err := os.ReadFile(configFile)
if err != nil {
log.Fatalf("Failed to read config file %s: %v", configFile, err)
}
cfg := &Config{}
if err := yaml.Unmarshal(data, cfg); err != nil {
log.Printf("Warning: malformed config file %s: %v, using defaults", configFile, err)
}
return applyDefaults(cfg, defaultPath, configFile)
}
// applyDefaults 以合理的默认值填充零值字段。
func applyDefaults(cfg *Config, defaultPath, configFile string) *Config {
// 如果整个 web 块为空(旧配置中没有 "web" 键),
// 填充默认端口,使应用仍能从 8080 启动。
if cfg.Web.Port == "" && cfg.Web.Socket == "" {
cfg.Web.Port = defaultPort
}
if len(cfg.Web.TrustedProxies) == 0 {
cfg.Web.TrustedProxies = defaultTrustedProxies
}
if cfg.Database.Type == "" {
cfg.Database.Type = "sqlite"
}
if cfg.Path == "" {
cfg.Path = defaultPath
}
if cfg.Secret == "" {
// 配置文件存在但没有密钥。拒绝启动:静默生成的回退值要么
// 可预测(旧的 hostname+pid 方案),要么导致每次重启都使所有会话失效。
log.Fatalf("Config file %s is missing a session secret. "+
"Add a random value, e.g. `secret: %s`, and restart.",
configFile, generateSecret())
}
return cfg
}