- 数据库默认文件 filecodebox.db → fileshare.db(config.go 默认值与全部文档/编排同步)
- Go module filecodebox → fileshare(全部 import 同步,build/vet/test 全绿)
- 应用版本 APP_VERSION 2.5.6 → 26.9(health 接口已验证返回 26.9)
- deploy 编排统一:compose 项目名、Postgres 默认凭据、minio 桶名、env 注释
- JWT issuer、存储临时目录前缀、web 包名同步 fileshare
- CI:镜像 tag 以 APP_VERSION 为唯一版本源,main/tag 推送即发布
${VER} + latest;tag 触发时校验 tag 名与 APP_VERSION 一致,防错版
- 本地开发库文件已改名 fileshare.db(含 -shm/-wal 清理)
173 lines
5.1 KiB
Go
173 lines
5.1 KiB
Go
// Package settings 提供数据库 settings KV 的运行时读写:
|
||
// env(FCB_*)提供基线,DB KV 覆盖可变项;管理端修改后立即生效。
|
||
package settings
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"log"
|
||
"sync"
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
|
||
"fileshare/internal/config"
|
||
"fileshare/internal/model"
|
||
)
|
||
|
||
// settingsKey 数据库中的配置键(对齐参考实现)。
|
||
const settingsKey = "settings"
|
||
|
||
// Manager 设置管理器:线程安全,缓存 KV 覆盖到内存。
|
||
type Manager struct {
|
||
db *gorm.DB
|
||
cfg *config.Config
|
||
|
||
mu sync.RWMutex
|
||
secret string // jwt_secret(频繁使用,单独缓存)
|
||
initPwd string // admin_token 哈希(频繁使用,单独缓存)
|
||
}
|
||
|
||
// NewManager 构造设置管理器并加载 DB KV。
|
||
// ensure 默认配置行(首次启动时写入 settings 键)。
|
||
func NewManager(ctx context.Context, db *gorm.DB, cfg *config.Config) (*Manager, error) {
|
||
m := &Manager{db: db, cfg: cfg}
|
||
if err := m.ensureSettingsRow(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := m.Reload(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
// ensureSettingsRow 首次启动时把默认安全配置写入 KV(对齐 ensure_settings_row)。
|
||
func (m *Manager) ensureSettingsRow(ctx context.Context) error {
|
||
var row model.KeyValue
|
||
err := m.db.WithContext(ctx).Where(model.KeyValue{Key: settingsKey}).First(&row).Error
|
||
if err == nil {
|
||
return nil
|
||
}
|
||
// 双方言:必须用 errors.Is 判定(sqlite 驱动错误链与字符串消息与 postgres 不同)
|
||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return err
|
||
}
|
||
// 不存在:写入初始配置(不含 admin_token/jwt_secret,保持未初始化状态)
|
||
initial := map[string]any{}
|
||
raw, _ := json.Marshal(initial)
|
||
row = model.KeyValue{Key: settingsKey, Value: strPtr(string(raw))}
|
||
if err := m.db.WithContext(ctx).Create(&row).Error; err != nil {
|
||
return err
|
||
}
|
||
log.Println("[settings] 系统尚未初始化,请在浏览器中打开站点并完成管理员密码设置")
|
||
return nil
|
||
}
|
||
|
||
// Reload 从数据库加载 settings KV 并覆盖到运行时配置。
|
||
func (m *Manager) Reload(ctx context.Context) error {
|
||
var row model.KeyValue
|
||
err := m.db.WithContext(ctx).Where(model.KeyValue{Key: settingsKey}).First(&row).Error
|
||
if err != nil {
|
||
// 行不存在时保持现有覆盖(双方言:errors.Is 判定)
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil
|
||
}
|
||
return err
|
||
}
|
||
kv := map[string]any{}
|
||
if row.Value != nil && *row.Value != "" {
|
||
if err := json.Unmarshal([]byte(*row.Value), &kv); err != nil {
|
||
log.Printf("[settings] settings KV 解析失败: %v", err)
|
||
}
|
||
}
|
||
// 内部键不允许通过 KV 覆盖(_ 开头)
|
||
safe := map[string]any{}
|
||
for k, v := range kv {
|
||
if len(k) > 0 && k[0] == '_' {
|
||
continue
|
||
}
|
||
safe[k] = v
|
||
}
|
||
m.mu.Lock()
|
||
m.cfg.ApplyKV(safe)
|
||
m.secret, _ = safe["jwt_secret"].(string)
|
||
m.initPwd, _ = safe["admin_token"].(string)
|
||
m.mu.Unlock()
|
||
return nil
|
||
}
|
||
|
||
// Get 返回当前配置(只读使用;不要修改返回值)。
|
||
func (m *Manager) Get() *config.Config { return m.cfg }
|
||
|
||
// SecretProvider 返回 jwt_secret 读取函数(JWT 中间件用)。
|
||
func (m *Manager) SecretProvider() func() string {
|
||
return func() string {
|
||
m.mu.RLock()
|
||
defer m.mu.RUnlock()
|
||
return m.secret
|
||
}
|
||
}
|
||
|
||
// IsInitialized 系统是否已完成初始化(管理员密码已设置且非默认密码)。
|
||
func (m *Manager) IsInitialized() bool {
|
||
m.mu.RLock()
|
||
defer m.mu.RUnlock()
|
||
if m.initPwd == "" {
|
||
return false
|
||
}
|
||
// 旧版默认密码视为未初始化(对齐 LEGACY_DEFAULT_ADMIN_TOKEN 检查)
|
||
return !verifyLegacyDefault(m.initPwd)
|
||
}
|
||
|
||
// legacyDefaultToken 参考实现的旧默认管理员密码。
|
||
const legacyDefaultToken = "FileCodeBox2023"
|
||
|
||
// verifyLegacyDefault 检查哈希是否对应旧默认密码。
|
||
func verifyLegacyDefault(hashed string) bool {
|
||
if hashed == "" {
|
||
return false
|
||
}
|
||
return VerifyPassword(legacyDefaultToken, hashed)
|
||
}
|
||
|
||
// UpdateKV 合并更新 settings KV(管理端保存配置)。
|
||
func (m *Manager) UpdateKV(ctx context.Context, patch map[string]any) error {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
// 读现有值
|
||
var row model.KeyValue
|
||
err := m.db.WithContext(ctx).Where(model.KeyValue{Key: settingsKey}).First(&row).Error
|
||
kv := map[string]any{}
|
||
if err == nil && row.Value != nil {
|
||
_ = json.Unmarshal([]byte(*row.Value), &kv)
|
||
}
|
||
for k, v := range patch {
|
||
if len(k) > 0 && k[0] == '_' {
|
||
continue
|
||
}
|
||
kv[k] = v
|
||
}
|
||
raw, err := json.Marshal(kv)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err == nil && row.ID > 0 {
|
||
row.Value = strPtr(string(raw))
|
||
return m.db.WithContext(ctx).Model(&row).Update("value", row.Value).Error
|
||
}
|
||
row = model.KeyValue{Key: settingsKey, Value: strPtr(string(raw))}
|
||
return m.db.WithContext(ctx).Create(&row).Error
|
||
}
|
||
|
||
// SystemStart 记录系统启动时间(对齐 sys_start 键)。
|
||
func (m *Manager) SystemStart(ctx context.Context) {
|
||
now := time.Now().UnixMilli()
|
||
raw, _ := json.Marshal(now)
|
||
_ = m.db.WithContext(ctx).Where(model.KeyValue{Key: "sys_start"}).
|
||
Assign(model.KeyValue{Value: strPtr(string(raw))}).
|
||
FirstOrCreate(&model.KeyValue{Key: "sys_start"}).Error
|
||
}
|
||
|
||
func strPtr(s string) *string { return &s }
|