- 生效规则统一为「后台填了用后台的,没填回落环境变量」,老部署不改 env 照常跑 - config.Resolver:短缓存解析有效配置,存储后端按配置签名热重建;后台保存主动失效 - 秘密项(client secret / bot token / R2 密钥 / 管理员密码)接口永不回显明文, 只报「是否已配置、来自哪里」;留空保存 = 保持现值 - 管理员密码 bcrypt 入库,DB 哈希优先、显式设置的 env 密码作解锁后路; 后台改过密码后 admin/admin 开发模式立即失效 - 设置页新增「登录与存储」标签,基础信息加站点地址;秘密项带来源提示 - 监听地址 / 数据库 / 目录 / ONE_SECRET / Passkey 仍留环境变量(启动期依赖)
144 lines
4.4 KiB
Go
144 lines
4.4 KiB
Go
// 系统设置的运行时解析层。
|
||
//
|
||
// 迁移自环境变量的设置(站点地址、OAuth 凭据、R2 存储、管理员账号)存进
|
||
// settings KV 表后,生效规则统一为「DB 有值用 DB,空值回落环境变量」:
|
||
// 老部署不改 env 也能照常跑,后台改过的值随时可以赢回来。
|
||
//
|
||
// Resolver 在 env Config 之上叠加 DB 值,给两个 API 提供按需解析的有效配置;
|
||
// 配置带短缓存(设置保存时主动失效),存储后端按配置签名惰性重建——
|
||
// 后台改完 R2 凭据不用重启,下一次上传就走新桶。
|
||
package config
|
||
|
||
import (
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"oneblog/internal/model"
|
||
"oneblog/internal/storage"
|
||
"oneblog/internal/store"
|
||
)
|
||
|
||
// Overlay 返回 c 的副本:model.Settings 里非空的系统设置项覆盖 env 值。
|
||
// 每个字段独立回落——比如 client_id 在后台改了、secret 还是 env 的,
|
||
// 两个拼起来照样能用,不必逼站主一次填齐。
|
||
func (c *Config) Overlay(st model.Settings) *Config {
|
||
e := *c
|
||
pick := func(dst *string, v string) {
|
||
if t := strings.TrimSpace(v); t != "" {
|
||
*dst = t
|
||
}
|
||
}
|
||
pick(&e.SiteURL, st.SiteURL)
|
||
pick(&e.GitHubClientID, st.GitHubClientID)
|
||
pick(&e.GitHubClientSecret, st.GitHubClientSecret)
|
||
pick(&e.GoogleClientID, st.GoogleClientID)
|
||
pick(&e.GoogleClientSecret, st.GoogleClientSecret)
|
||
pick(&e.TelegramBot, st.TelegramBot)
|
||
pick(&e.TelegramToken, st.TelegramToken)
|
||
pick(&e.AdminUser, st.AdminUsername)
|
||
pick(&e.UploadsPublicBase, st.UploadsPublicBase)
|
||
pick(&e.S3Endpoint, st.S3Endpoint)
|
||
pick(&e.R2Bucket, st.R2Bucket)
|
||
pick(&e.R2AccessKey, st.R2AccessKey)
|
||
pick(&e.R2SecretKey, st.R2SecretKey)
|
||
// 存储端按「凭据是否齐全」重新判定,与 config.Load 的规则一致
|
||
if e.S3Endpoint != "" && e.R2Bucket != "" && e.R2AccessKey != "" && e.R2SecretKey != "" {
|
||
e.StorageDriver = "r2"
|
||
} else {
|
||
e.StorageDriver = "local"
|
||
}
|
||
return &e
|
||
}
|
||
|
||
// SettingsSource 是 Resolver 对 store 的最小依赖(测试可注入假实现)。
|
||
type SettingsSource interface {
|
||
GetSettings() (model.Settings, error)
|
||
}
|
||
|
||
// Resolver 提供带缓存的有效配置与按需重建的存储后端。零值不可用,
|
||
// 用 NewResolver 构造;API 拿不到 Resolver 时(旧测试)直接用 env 值。
|
||
type Resolver struct {
|
||
env *Config
|
||
st SettingsSource
|
||
|
||
mu sync.Mutex
|
||
cfg *Config
|
||
cfgAt time.Time
|
||
blobs storage.BlobStore
|
||
blobsKV string
|
||
}
|
||
|
||
// cacheTTL 只兜「绕过后台直接改库」这种少数派路径;正常保存走
|
||
// Invalidate() 立即生效,所以 TTL 可以放宽到不影响体验的量级。
|
||
const cacheTTL = 10 * time.Second
|
||
|
||
func NewResolver(env *Config, st SettingsSource) *Resolver {
|
||
return &Resolver{env: env, st: st}
|
||
}
|
||
|
||
// Config 返回叠加了 DB 设置的有效配置。读库失败时回落 env——
|
||
// 设置页不能因为 DB 抖动把整站带挂。
|
||
func (r *Resolver) Config() *Config {
|
||
if r == nil {
|
||
return r.env
|
||
}
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
if r.cfg != nil && time.Since(r.cfgAt) < cacheTTL {
|
||
return r.cfg
|
||
}
|
||
st, err := r.st.GetSettings()
|
||
if err != nil {
|
||
if r.cfg == nil {
|
||
return r.env
|
||
}
|
||
return r.cfg // 沿用上次缓存,别让一次查询失败打断请求
|
||
}
|
||
r.cfg = r.env.Overlay(st)
|
||
r.cfgAt = time.Now()
|
||
return r.cfg
|
||
}
|
||
|
||
// Invalidate 丢弃缓存:后台保存设置后调用,改完立即生效。
|
||
func (r *Resolver) Invalidate() {
|
||
if r == nil {
|
||
return
|
||
}
|
||
r.mu.Lock()
|
||
r.cfg, r.cfgAt = nil, time.Time{}
|
||
r.mu.Unlock()
|
||
}
|
||
|
||
// Blobs 返回当前有效配置对应的存储后端,配置变了自动重建。
|
||
// 构造失败(R2 凭据不对等)返回 nil,调用方按「无存储」处理。
|
||
func (r *Resolver) Blobs() storage.BlobStore {
|
||
if r == nil {
|
||
return nil
|
||
}
|
||
c := r.Config()
|
||
kv := c.StorageDriver + "|" + c.S3Endpoint + "|" + c.R2Bucket + "|" +
|
||
c.R2AccessKey + "|" + c.R2SecretKey
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
if r.blobs != nil && kv == r.blobsKV {
|
||
return r.blobs
|
||
}
|
||
var b storage.BlobStore
|
||
switch c.StorageDriver {
|
||
case "r2":
|
||
s, err := storage.NewR2(c.S3Endpoint, c.R2Bucket, c.R2AccessKey, c.R2SecretKey)
|
||
if err != nil {
|
||
return r.blobs // 重建失败沿用旧后端,至少不把上传/读取打断
|
||
}
|
||
b = s
|
||
default:
|
||
b = storage.NewLocal(c.DataDir + "/uploads")
|
||
}
|
||
r.blobs, r.blobsKV = b, kv
|
||
return b
|
||
}
|
||
|
||
// 静态断言:*store.Store 满足 SettingsSource(GetSettings 在 store 包里)。
|
||
var _ SettingsSource = (*store.Store)(nil)
|