mirror of
https://github.com/DullJZ/s3-balance.git
synced 2026-09-05 15:56:39 +08:00
Feat: Config Manager API
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Manager 配置管理器,支持热更新
|
||||
@@ -227,6 +229,181 @@ func (m *Manager) logConfigChanges(oldConfig, newConfig *Config) {
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateConfig 通过 API 更新配置文件
|
||||
// 返回错误如果验证失败或写入失败
|
||||
func (m *Manager) UpdateConfig(newConfig *Config) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
// 1. 验证新配置
|
||||
if err := m.validateConfig(newConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 备份当前配置文件
|
||||
if err := m.backupConfigFile(); err != nil {
|
||||
log.Printf("Failed to backup config file: %v", err)
|
||||
// 继续执行,备份失败不应阻止更新
|
||||
}
|
||||
|
||||
// 3. 将新配置写入文件
|
||||
if err := m.writeConfigFile(newConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. 更新内存中的配置
|
||||
oldConfig := m.config
|
||||
m.config = newConfig
|
||||
|
||||
// 5. 更新最后修改时间,避免文件监听重复触发
|
||||
if fileInfo, err := os.Stat(m.configFile); err == nil {
|
||||
m.lastModTime = fileInfo.ModTime()
|
||||
}
|
||||
|
||||
log.Printf("Configuration updated successfully via API")
|
||||
|
||||
// 6. 触发配置变更回调(在锁外执行)
|
||||
callbacks := make([]func(*Config), len(m.callbacks))
|
||||
copy(callbacks, m.callbacks)
|
||||
|
||||
go func() {
|
||||
for _, callback := range callbacks {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("Config change callback panic: %v", r)
|
||||
}
|
||||
}()
|
||||
callback(newConfig)
|
||||
}()
|
||||
}
|
||||
}()
|
||||
|
||||
// 7. 记录配置变更
|
||||
m.logConfigChanges(oldConfig, newConfig)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateConfig 验证配置的有效性
|
||||
func (m *Manager) validateConfig(cfg *Config) error {
|
||||
// 基本验证
|
||||
if cfg.Server.Port <= 0 || cfg.Server.Port > 65535 {
|
||||
return fmt.Errorf("invalid server port: %d", cfg.Server.Port)
|
||||
}
|
||||
|
||||
if len(cfg.Buckets) == 0 {
|
||||
return fmt.Errorf("at least one bucket is required")
|
||||
}
|
||||
|
||||
// 验证存储桶配置
|
||||
for i, bucket := range cfg.Buckets {
|
||||
if bucket.Name == "" {
|
||||
return fmt.Errorf("bucket[%d]: name is required", i)
|
||||
}
|
||||
|
||||
// 虚拟存储桶不需要端点和凭据
|
||||
if !bucket.Virtual {
|
||||
if bucket.Endpoint == "" {
|
||||
return fmt.Errorf("bucket[%d] (%s): endpoint is required for non-virtual bucket", i, bucket.Name)
|
||||
}
|
||||
if bucket.AccessKeyID == "" {
|
||||
return fmt.Errorf("bucket[%d] (%s): access_key_id is required for non-virtual bucket", i, bucket.Name)
|
||||
}
|
||||
if bucket.SecretAccessKey == "" {
|
||||
return fmt.Errorf("bucket[%d] (%s): secret_access_key is required for non-virtual bucket", i, bucket.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// 解析并验证容量大小
|
||||
if err := cfg.Buckets[i].ParseMaxSize(); err != nil {
|
||||
return fmt.Errorf("bucket[%d] (%s): invalid max_size: %w", i, bucket.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证负载均衡策略
|
||||
validStrategies := map[string]bool{
|
||||
"round-robin": true,
|
||||
"least-space": true,
|
||||
"weighted": true,
|
||||
}
|
||||
if !validStrategies[cfg.Balancer.Strategy] {
|
||||
return fmt.Errorf("invalid balancer strategy: %s (must be one of: round-robin, least-space, weighted)", cfg.Balancer.Strategy)
|
||||
}
|
||||
|
||||
// 验证数据库配置
|
||||
if cfg.Database.Type == "" {
|
||||
return fmt.Errorf("database type is required")
|
||||
}
|
||||
validDBTypes := map[string]bool{
|
||||
"sqlite": true,
|
||||
"mysql": true,
|
||||
"postgres": true,
|
||||
}
|
||||
if !validDBTypes[cfg.Database.Type] {
|
||||
return fmt.Errorf("invalid database type: %s (must be one of: sqlite, mysql, postgres)", cfg.Database.Type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// backupConfigFile 备份当前配置文件
|
||||
func (m *Manager) backupConfigFile() error {
|
||||
backupPath := m.configFile + ".backup." + time.Now().Format("20060102-150405")
|
||||
|
||||
sourceData, err := os.ReadFile(m.configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(backupPath, sourceData, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write backup file: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Config file backed up to: %s", backupPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeConfigFile 将配置写入 YAML 文件
|
||||
func (m *Manager) writeConfigFile(cfg *Config) error {
|
||||
// 临时文件,确保原子性
|
||||
tmpFile := m.configFile + ".tmp"
|
||||
|
||||
file, err := os.OpenFile(tmpFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp config file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := yaml.NewEncoder(file)
|
||||
encoder.SetIndent(2)
|
||||
|
||||
if err := encoder.Encode(cfg); err != nil {
|
||||
file.Close()
|
||||
os.Remove(tmpFile)
|
||||
return fmt.Errorf("failed to encode config: %w", err)
|
||||
}
|
||||
|
||||
if err := encoder.Close(); err != nil {
|
||||
file.Close()
|
||||
os.Remove(tmpFile)
|
||||
return fmt.Errorf("failed to close encoder: %w", err)
|
||||
}
|
||||
|
||||
if err := file.Close(); err != nil {
|
||||
os.Remove(tmpFile)
|
||||
return fmt.Errorf("failed to close temp file: %w", err)
|
||||
}
|
||||
|
||||
// 原子性替换原文件
|
||||
if err := os.Rename(tmpFile, m.configFile); err != nil {
|
||||
os.Remove(tmpFile)
|
||||
return fmt.Errorf("failed to replace config file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭配置管理器
|
||||
func (m *Manager) Close() error {
|
||||
// 停止监听协程
|
||||
|
||||
Reference in New Issue
Block a user