Files

455 lines
15 KiB
Go

package config
import (
"fmt"
"net"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"cloudnest/internal/pkg/logger"
"github.com/spf13/viper"
)
// Config holds all application configuration.
// Config 包含所有应用程序配置。
//
// Configuration loading order (later overrides earlier):
// 1. config.yaml (default values)
// 2. config.{env}.yaml (environment-specific values)
// 3. Environment variables (highest priority, auto-mapped)
//
// 配置加载顺序(后面的覆盖前面的):
// 1. config.yaml(默认值)
// 2. config.{env}.yaml(环境特定值)
// 3. 环境变量(最高优先级,自动映射)
type Config struct {
App AppConfig `mapstructure:"app"`
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Cache CacheConfig `mapstructure:"cache"`
Storage StorageConfig `mapstructure:"storage"`
JWT JWTConfig `mapstructure:"jwt"`
Email EmailConfig `mapstructure:"email"`
}
// AppConfig holds application-level configuration.
// AppConfig 包含应用程序级别的配置。
type AppConfig struct {
Name string `mapstructure:"name"`
Version string `mapstructure:"version"`
Env string `mapstructure:"env"`
LogLevel string `mapstructure:"log_level"`
FileServiceURL string `mapstructure:"file_service_url"`
}
// ServerConfig holds HTTP server configuration.
// ServerConfig 包含 HTTP 服务器配置。
type ServerConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
GracefulShutdownTimeout int `mapstructure:"graceful_shutdown_timeout"`
}
// DatabaseConfig holds database connection configuration.
// DatabaseConfig 包含数据库连接配置。
type DatabaseConfig struct {
Driver string `mapstructure:"driver"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"dbname"`
Charset string `mapstructure:"charset"`
ParseTime bool `mapstructure:"parse_time"`
Loc string `mapstructure:"loc"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
MaxOpenConns int `mapstructure:"max_open_conns"`
ConnMaxLifetime int `mapstructure:"conn_max_lifetime"`
}
// CacheConfig holds Redis cache configuration.
// CacheConfig 包含 Redis 缓存配置。
type CacheConfig struct {
Driver string `mapstructure:"driver"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
PoolSize int `mapstructure:"pool_size"`
MinIdleConns int `mapstructure:"min_idle_conns"`
}
// StorageConfig holds MinIO object storage configuration.
// StorageConfig 包含 MinIO 对象存储配置。
type StorageConfig struct {
Driver string `mapstructure:"driver"`
Endpoint string `mapstructure:"endpoint"`
AccessKeyID string `mapstructure:"access_key_id"`
SecretAccessKey string `mapstructure:"secret_access_key"`
BucketName string `mapstructure:"bucket_name"`
UseSSL bool `mapstructure:"use_ssl"`
PresignDuration int `mapstructure:"presign_duration"`
MaxFileSize int `mapstructure:"max_file_size"`
RecycleBinExpireDays int `mapstructure:"recycle_bin_expire_days"`
}
// JWTConfig holds JWT authentication configuration.
// JWTConfig 包含 JWT 认证配置。
type JWTConfig struct {
Secret string `mapstructure:"secret"`
ExpiresIn int `mapstructure:"expires_in"`
Issuer string `mapstructure:"issuer"`
}
// EmailConfig holds SMTP email configuration.
// EmailConfig 包含 SMTP 邮件配置。
type EmailConfig struct {
SMTPHost string `mapstructure:"smtp_host"`
SMTPPort int `mapstructure:"smtp_port"`
SMTPUsername string `mapstructure:"smtp_username"`
SMTPPassword string `mapstructure:"smtp_password"`
FromName string `mapstructure:"from_name"`
}
// DSN builds MySQL DSN string from config fields.
// DSN 从配置字段构建 MySQL DSN 字符串。
//
// Returns:
// - string: The formatted DSN string.
//
// 返回值:
// - string: 格式化后的 DSN 字符串。
func (c *DatabaseConfig) DSN() string {
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=%t&loc=%s",
c.User,
c.Password,
c.Host,
c.Port,
c.DBName,
c.Charset,
c.ParseTime,
c.Loc,
)
}
// Addr builds Redis connection address.
// Addr 构建 Redis 连接地址。
//
// Returns:
// - string: The formatted "host:port" address.
//
// 返回值:
// - string: 格式化的 "host:port" 地址。
func (c *CacheConfig) Addr() string {
return fmt.Sprintf("%s:%d", c.Host, c.Port)
}
// PresignDuration builds time.Duration from hours config.
// PresignDuration 从小时配置构建 time.Duration。
//
// Returns:
// - time.Duration: The presign duration.
//
// 返回值:
// - time.Duration: 预签名有效期。
func (c *StorageConfig) PresignDurationTime() time.Duration {
return time.Duration(c.PresignDuration) * time.Hour
}
// ExpiresIn builds time.Duration from hours config.
// ExpiresIn 从小时配置构建 time.Duration。
//
// Returns:
// - time.Duration: The JWT expiration duration.
//
// 返回值:
// - time.Duration: JWT 过期时间。
func (c *JWTConfig) ExpiresInDuration() time.Duration {
return time.Duration(c.ExpiresIn) * time.Hour
}
// getEnv returns the current environment name from CLOUDNEST_ENV or defaults to "dev".
// getEnv 从 CLOUDNEST_ENV 获取当前环境名称,默认为 "dev"。
func getEnv() string {
env := strings.ToLower(os.Getenv("CLOUDNEST_ENV"))
if env == "" {
env = "dev"
}
return env
}
// getConfigDir returns the absolute path to the configs directory.
// getConfigDir 返回 configs 目录的绝对路径。
//
// Note:
// - Tries to find configs/ relative to the executable or current working directory.
// - Falls back to current working directory if not found.
//
// 注意:
// - 尝试从可执行文件或当前工作目录相对位置找到 configs/。
// - 如果找不到则回退到当前工作目录。
func getConfigDir() string {
// Try to get the directory of the current file
// 尝试获取当前文件的目录
_, filename, _, ok := runtime.Caller(0)
if ok {
dir := filepath.Join(filepath.Dir(filename), "..", "..", "configs")
if _, err := os.Stat(dir); err == nil {
return dir
}
}
// Try executable directory / 尝试可执行文件目录
ex, err := os.Executable()
if err == nil {
dir := filepath.Join(filepath.Dir(ex), "configs")
if _, err := os.Stat(dir); err == nil {
return dir
}
}
// Try current working directory / 尝试当前工作目录
cwd, err := os.Getwd()
if err == nil {
dir := filepath.Join(cwd, "configs")
if _, err := os.Stat(dir); err == nil {
return dir
}
}
return "configs"
}
// Load reads and parses configuration from YAML files and environment variables.
// Load 从 YAML 文件和环境变量中读取并解析配置。
//
// Loading order (later overrides earlier):
// 1. configs/config.yaml (base defaults)
// 2. configs/config.{env}.yaml (env-specific overrides)
// 3. Environment variables (highest priority)
//
// Environment selection via CLOUDNEST_ENV:
// - "dev" -> configs/config.dev.yaml
// - "test" -> configs/config.test.yaml
// - "prod" -> configs/config.prod.yaml
// - default -> configs/config.dev.yaml
//
// Environment variables are automatically mapped with the following prefixes:
// - APP_ -> app
// - SERVER_ -> server
// - DB_ -> database
// - REDIS_ -> cache
// - MINIO_ -> storage
// - JWT_ -> jwt
// - EMAIL_ -> email
//
// Returns:
// - *Config: A pointer to the parsed configuration struct.
//
// 配置加载顺序(后面的覆盖前面的):
// 1. configs/config.yaml(基础默认值)
// 2. configs/config.{env}.yaml(环境特定覆盖)
// 3. 环境变量(最高优先级)
//
// 通过 CLOUDNEST_ENV 选择环境:
// - "dev" -> configs/config.dev.yaml
// - "test" -> configs/config.test.yaml
// - "prod" -> configs/config.prod.yaml
// - 默认 -> configs/config.dev.yaml
//
// 环境变量自动映射的前缀:
// - APP_ -> app
// - SERVER_ -> server
// - DB_ -> database
// - REDIS_ -> cache
// - MINIO_ -> storage
// - JWT_ -> jwt
// - EMAIL_ -> email
//
// 返回值:
// - *Config: 解析后的配置结构体指针。
func Load() *Config {
env := getEnv()
configDir := getConfigDir()
v := viper.New()
v.SetConfigType("yaml")
// 1. Load base config / 加载基础配置
baseConfig := filepath.Join(configDir, "config.yaml")
v.SetConfigFile(baseConfig)
if err := v.ReadInConfig(); err != nil {
logger.Warn("failed to read base config, using defaults", "error", err, "path", baseConfig)
} else {
logger.Info("base config loaded", "path", baseConfig)
}
// 2. Load environment-specific config / 加载环境特定配置
envConfig := filepath.Join(configDir, fmt.Sprintf("config.%s.yaml", env))
if _, err := os.Stat(envConfig); err == nil {
envViper := viper.New()
envViper.SetConfigFile(envConfig)
if err := envViper.ReadInConfig(); err == nil {
// Merge environment config into base config
// 将环境配置合并到基础配置中
for _, key := range envViper.AllKeys() {
v.Set(key, envViper.Get(key))
}
logger.Info("env config loaded", "env", env, "path", envConfig)
} else {
logger.Warn("failed to read env config", "env", env, "error", err)
}
} else {
logger.Info("env config not found, using base config", "env", env, "path", envConfig)
}
// 3. Enable environment variable overrides / 启用环境变量覆盖
v.AutomaticEnv()
v.SetEnvPrefix("")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// Map environment variables to nested config keys
// 将环境变量映射到嵌套配置键
envMappings := map[string]string{
"APP_NAME": "app.name",
"APP_VERSION": "app.version",
"APP_ENV": "app.env",
"APP_LOG_LEVEL": "app.log_level",
"APP_FILE_SERVICE_URL": "app.file_service_url",
"SERVER_HOST": "server.host",
"SERVER_PORT": "server.port",
"SERVER_GRACEFUL_SHUTDOWN_TIMEOUT": "server.graceful_shutdown_timeout",
"DB_DRIVER": "database.driver",
"DB_HOST": "database.host",
"DB_PORT": "database.port",
"DB_USER": "database.user",
"DB_PASSWORD": "database.password",
"DB_NAME": "database.dbname",
"DB_CHARSET": "database.charset",
"DB_PARSE_TIME": "database.parse_time",
"DB_LOC": "database.loc",
"DB_MAX_IDLE_CONNS": "database.max_idle_conns",
"DB_MAX_OPEN_CONNS": "database.max_open_conns",
"DB_CONN_MAX_LIFETIME": "database.conn_max_lifetime",
"REDIS_HOST": "cache.host",
"REDIS_PORT": "cache.port",
"REDIS_PASSWORD": "cache.password",
"REDIS_DB": "cache.db",
"REDIS_POOL_SIZE": "cache.pool_size",
"REDIS_MIN_IDLE_CONNS": "cache.min_idle_conns",
"MINIO_ENDPOINT": "storage.endpoint",
"MINIO_ACCESS_KEY": "storage.access_key_id",
"MINIO_SECRET_KEY": "storage.secret_access_key",
"MINIO_BUCKET": "storage.bucket_name",
"MINIO_USE_SSL": "storage.use_ssl",
"MINIO_PRESIGN_DURATION": "storage.presign_duration",
"MINIO_MAX_FILE_SIZE": "storage.max_file_size",
"MINIO_RECYCLE_BIN_EXPIRE_DAYS": "storage.recycle_bin_expire_days",
"JWT_SECRET": "jwt.secret",
"JWT_EXPIRES_IN": "jwt.expires_in",
"JWT_ISSUER": "jwt.issuer",
"EMAIL_SMTP_HOST": "email.smtp_host",
"EMAIL_SMTP_PORT": "email.smtp_port",
"EMAIL_SMTP_USERNAME": "email.smtp_username",
"EMAIL_SMTP_PASSWORD": "email.smtp_password",
"EMAIL_FROM_NAME": "email.from_name",
}
for envKey, configKey := range envMappings {
if val := os.Getenv(envKey); val != "" {
// Try to parse as int/bool first, fallback to string
// 先尝试解析为 int/bool,否则回退为 string
if intVal, err := strconv.Atoi(val); err == nil {
v.Set(configKey, intVal)
} else if boolVal, err := strconv.ParseBool(val); err == nil {
v.Set(configKey, boolVal)
} else {
v.Set(configKey, val)
}
}
}
// 4. Unmarshal into struct / 反序列化到结构体
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
logger.Fatal("failed to unmarshal config", "error", err)
}
logger.Info("config loaded",
"app", fmt.Sprintf("%s:%s", cfg.App.Name, cfg.App.Version),
"env", cfg.App.Env,
"config_dir", configDir,
)
return &cfg
}
// IsDev returns true if running in development environment.
// IsDev 如果运行在开发环境则返回 true。
func (c *Config) IsDev() bool {
return strings.ToLower(c.App.Env) == "development" || strings.ToLower(c.App.Env) == "dev"
}
// IsProd returns true if running in production environment.
// IsProd 如果运行在生产环境则返回 true。
func (c *Config) IsProd() bool {
return strings.ToLower(c.App.Env) == "production" || strings.ToLower(c.App.Env) == "prod"
}
// IsTest returns true if running in test environment.
// IsTest 如果运行在测试环境则返回 true。
func (c *Config) IsTest() bool {
return strings.ToLower(c.App.Env) == "test"
}
// FindAvailablePort checks if the configured port is available, returns next available if occupied.
// FindAvailablePort 检查配置的端口是否可用,如果被占用则返回下一个可用端口。
//
// Parameters:
// - preferredPort: The preferred port to use.
// - maxAttempts: Maximum number of ports to try.
//
// Returns:
// - int: An available port number.
// - error: An error if no port is available after max attempts.
//
// Note:
// - Only searches in development mode. In production, it returns the configured port directly.
// - Search range: preferredPort to preferredPort + maxAttempts - 1.
//
// 参数:
// - preferredPort: 首选端口。
// - maxAttempts: 最大尝试次数。
//
// 返回值:
// - int: 可用端口号。
// - error: 如果超过最大尝试次数仍未找到可用端口则返回错误。
//
// 注意:
// - 仅在开发模式下搜索。生产模式下直接返回配置端口。
// - 搜索范围: preferredPort 到 preferredPort + maxAttempts - 1。
func FindAvailablePort(preferredPort int, maxAttempts int) (int, error) {
for i := 0; i < maxAttempts; i++ {
port := preferredPort + i
addr := fmt.Sprintf(":%d", port)
listener, err := net.Listen("tcp", addr)
if err != nil {
logger.Warn("port occupied, trying next", "port", port, "error", err)
continue
}
listener.Close()
return port, nil
}
return 0, fmt.Errorf("no available port found in range %d-%d", preferredPort, preferredPort+maxAttempts-1)
}