Files
rill/config.go
T
2026-09-17 12:50:54 +08:00

221 lines
5.6 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 main
import (
"errors"
"fmt"
"io/fs"
"log/slog"
"os"
"strings"
"time"
"github.com/goccy/go-yaml"
)
type Config struct {
Server ServerConfig `yaml:"server"`
Log LogConfig `yaml:"log"`
Static StaticConfig `yaml:"static"`
API APIConfig `yaml:"api"`
Database DatabaseConfig `yaml:"database"`
}
type ServerConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Mode string `yaml:"mode"`
}
type LogConfig struct {
Level string `yaml:"level"`
AccessLog bool `yaml:"access_log"`
}
type StaticConfig struct {
Dir string `yaml:"dir"`
}
type APIConfig struct {
Prefix string `yaml:"prefix"`
CORS CORSConfig `yaml:"cors"`
}
type CORSConfig struct {
Enabled bool `yaml:"enabled"`
AllowOrigins []string `yaml:"allow_origins"`
AllowMethods []string `yaml:"allow_methods"`
AllowHeaders []string `yaml:"allow_headers"`
AllowCredentials bool `yaml:"allow_credentials"`
MaxAge string `yaml:"max_age"`
}
type DatabaseConfig struct {
Driver string `yaml:"driver"` // sqlite3 / mysql
ConnectTimeout string `yaml:"connect_timeout"` // 建立连接超时
SQLite SQLiteConfig `yaml:"sqlite"`
MySQL MySQLConfig `yaml:"mysql"`
}
type SQLiteConfig struct {
Path string `yaml:"path"`
}
type MySQLConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
Password string `yaml:"password"`
Database string `yaml:"database"`
Charset string `yaml:"charset"`
MaxOpenConns int `yaml:"max_open_conns"`
MaxIdleConns int `yaml:"max_idle_conns"`
ConnMaxLifetime string `yaml:"conn_max_lifetime"`
}
func defaultConfig() *Config {
return &Config{
Server: ServerConfig{
Host: "0.0.0.0",
Port: 8080,
Mode: "debug",
},
Log: LogConfig{
Level: "info",
AccessLog: true,
},
Static: StaticConfig{
Dir: "./dist",
},
API: APIConfig{
Prefix: "/api",
CORS: CORSConfig{
Enabled: true,
AllowOrigins: []string{"http://localhost:5173"},
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
AllowCredentials: false,
MaxAge: "12h",
},
},
Database: DatabaseConfig{
Driver: "sqlite3",
ConnectTimeout: "10s",
SQLite: SQLiteConfig{
Path: "./data/rill.db",
},
MySQL: MySQLConfig{
Host: "127.0.0.1",
Port: 3306,
User: "root",
Password: "",
Database: "rill",
Charset: "utf8mb4",
MaxOpenConns: 10,
MaxIdleConns: 5,
ConnMaxLifetime: "1h",
},
},
}
}
// LoadConfig 读取 YAML 配置文件,未指定的配置项使用默认值;文件不存在时回退到默认配置。
func LoadConfig(path string) (*Config, error) {
cfg := defaultConfig()
data, err := os.ReadFile(path)
switch {
case err == nil:
if err := yaml.Unmarshal(data, cfg); err != nil {
return nil, fmt.Errorf("解析配置文件 %s 失败: %w", path, err)
}
case errors.Is(err, fs.ErrNotExist):
slog.Warn("配置文件不存在,使用默认配置", "path", path)
default:
return nil, fmt.Errorf("读取配置文件 %s 失败: %w", path, err)
}
if err := cfg.validate(); err != nil {
return nil, err
}
return cfg, nil
}
func (c *Config) validate() error {
switch c.Server.Mode {
case "debug", "release", "test":
default:
return fmt.Errorf("server.mode 无效: %q(可选: debug/release/test", c.Server.Mode)
}
if c.Server.Port <= 0 || c.Server.Port > 65535 {
return fmt.Errorf("server.port 无效: %d", c.Server.Port)
}
if _, err := parseLogLevel(c.Log.Level); err != nil {
return err
}
if _, err := time.ParseDuration(c.API.CORS.MaxAge); err != nil {
return fmt.Errorf("api.cors.max_age 无效: %w", err)
}
if err := c.validateDatabase(); err != nil {
return err
}
return nil
}
func (c *Config) validateDatabase() error {
if _, err := time.ParseDuration(c.Database.ConnectTimeout); err != nil {
return fmt.Errorf("database.connect_timeout 无效: %w", err)
}
switch c.Database.Driver {
case "sqlite3":
if strings.TrimSpace(c.Database.SQLite.Path) == "" {
return fmt.Errorf("database.sqlite.path 不能为空")
}
case "mysql":
db := c.Database.MySQL
if strings.TrimSpace(db.Host) == "" {
return fmt.Errorf("database.mysql.host 不能为空")
}
if db.Port <= 0 || db.Port > 65535 {
return fmt.Errorf("database.mysql.port 无效: %d", db.Port)
}
if strings.TrimSpace(db.Database) == "" {
return fmt.Errorf("database.mysql.database 不能为空")
}
if db.MaxOpenConns < 0 || db.MaxIdleConns < 0 {
return fmt.Errorf("database.mysql 连接池大小不能为负数")
}
if _, err := time.ParseDuration(db.ConnMaxLifetime); err != nil {
return fmt.Errorf("database.mysql.conn_max_lifetime 无效: %w", err)
}
default:
return fmt.Errorf("database.driver 无效: %q(可选: sqlite3/mysql", c.Database.Driver)
}
return nil
}
func (c *Config) LogLevel() slog.Level {
level, _ := parseLogLevel(c.Log.Level)
return level
}
func (c *Config) CORSMaxAge() time.Duration {
maxAge, _ := time.ParseDuration(c.API.CORS.MaxAge)
return maxAge
}
func parseLogLevel(level string) (slog.Level, error) {
switch level {
case "debug":
return slog.LevelDebug, nil
case "info":
return slog.LevelInfo, nil
case "warn":
return slog.LevelWarn, nil
case "error":
return slog.LevelError, nil
default:
return slog.LevelInfo, fmt.Errorf("log.level 无效: %q(可选: debug/info/warn/error", level)
}
}