系统设置迁入后台:站点地址 / OAuth 凭据 / R2 存储 / 管理员账号,保存即生效无需重启
- 生效规则统一为「后台填了用后台的,没填回落环境变量」,老部署不改 env 照常跑 - config.Resolver:短缓存解析有效配置,存储后端按配置签名热重建;后台保存主动失效 - 秘密项(client secret / bot token / R2 密钥 / 管理员密码)接口永不回显明文, 只报「是否已配置、来自哪里」;留空保存 = 保持现值 - 管理员密码 bcrypt 入库,DB 哈希优先、显式设置的 env 密码作解锁后路; 后台改过密码后 admin/admin 开发模式立即失效 - 设置页新增「登录与存储」标签,基础信息加站点地址;秘密项带来源提示 - 监听地址 / 数据库 / 目录 / ONE_SECRET / Passkey 仍留环境变量(启动期依赖)
This commit is contained in:
1 parent
bf3ce934dd
commit
e68400b389
23 files changed
+957
-126
No files matched your search
@@ -0,0 +1,143 @@
|
||||
// 系统设置的运行时解析层。
|
||||
//
|
||||
// 迁移自环境变量的设置(站点地址、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)
|
||||
Reference in new issue
Block a user