Files
code-utils/filestorage.go

153 lines
4.4 KiB
Go
Raw Normal View History

2026-08-14 07:52:01 +08:00
package main
// filestorage.go 全局「文件存储方式」配置:由管理员(云端账号 id=1在设置页配置
2026-08-15 17:18:00 +08:00
// 权威数据经 nl-pms-api settings全局键 file_storage
// 保存时直接写 API 立即生效;各端上传图片时实时拉取该配置(短 TTL 缓存),
2026-08-14 07:52:01 +08:00
// 拉取失败(离线/未配置同步)回退本地缓存副本(由同步循环下发,见 syncFileStorageConfig
import (
"encoding/json"
"errors"
"net/http"
"strings"
"time"
)
const (
fileStorageKey = "file_storage" // settings 表 KV 键本地缓存JSON
fileStorageAtKey = "file_storage_updated_at" // LWW 时间戳,同步推拉的判定依据
fileStorageTTL = 30 * time.Second // 实时配置的内存缓存时长,避免连续上传反复查库
)
func normalizeFileStorage(c FileStorageConfig) FileStorageConfig {
if c.Mode != "server" {
c.Mode = "local"
}
c.BaseURL = strings.TrimRight(strings.TrimSpace(c.BaseURL), "/")
c.APIKey = strings.TrimSpace(c.APIKey)
return c
}
2026-08-15 17:18:00 +08:00
// defaultFileStorage 未配置时的默认:服务器模式,地址与打包同步 API 基址一致。
func (a *App) defaultFileStorage() FileStorageConfig {
return normalizeFileStorage(FileStorageConfig{
Mode: "server",
BaseURL: a.apiBaseURL(),
})
}
2026-08-14 07:52:01 +08:00
func (a *App) fileStorageConfig() FileStorageConfig {
var c FileStorageConfig
if raw := a.store.Meta(fileStorageKey); raw != "" {
_ = json.Unmarshal([]byte(raw), &c)
2026-08-15 17:18:00 +08:00
return normalizeFileStorage(c)
2026-08-14 07:52:01 +08:00
}
2026-08-15 17:18:00 +08:00
return a.defaultFileStorage()
2026-08-14 07:52:01 +08:00
}
2026-08-15 17:18:00 +08:00
// fetchRemoteFileStorage 从 API 读权威配置。第二个返回值表示"远端可用且结果权威"
// (无行记录时回落默认服务器配置);成功时顺带刷新本地缓存副本,供离线兜底。
2026-08-14 07:52:01 +08:00
func (a *App) fetchRemoteFileStorage() (FileStorageConfig, bool) {
2026-08-15 17:18:00 +08:00
if a.syncUserID() <= 0 || a.store.Meta("sync_access_token") == "" || a.apiBaseURL() == "" {
2026-08-14 07:52:01 +08:00
return FileStorageConfig{}, false
}
2026-08-15 17:18:00 +08:00
val, at, ok, e := a.apiGetGlobalSetting(fileStorageKey)
2026-08-14 07:52:01 +08:00
if e != nil {
return FileStorageConfig{}, false
}
2026-08-15 17:18:00 +08:00
if !ok {
return a.defaultFileStorage(), true
}
2026-08-14 07:52:01 +08:00
var c FileStorageConfig
if json.Unmarshal([]byte(val), &c) != nil {
return FileStorageConfig{}, false
}
c = normalizeFileStorage(c)
if b, e := json.Marshal(c); e == nil {
_ = a.store.SetMeta(fileStorageKey, string(b))
_ = a.store.SetMeta(fileStorageAtKey, at)
}
return c, true
}
func (a *App) currentFileStorage() FileStorageConfig {
a.fsMu.Lock()
if !a.fsCfgAt.IsZero() && time.Since(a.fsCfgAt) < fileStorageTTL {
c := a.fsCfg
a.fsMu.Unlock()
return c
}
a.fsMu.Unlock()
c, ok := a.fetchRemoteFileStorage()
if !ok {
c = a.fileStorageConfig()
}
a.fsMu.Lock()
a.fsCfg, a.fsCfgAt = c, time.Now()
a.fsMu.Unlock()
return c
}
func (a *App) invalidateFileStorageCache() {
a.fsMu.Lock()
a.fsCfgAt = time.Time{}
a.fsMu.Unlock()
}
func (a *App) GetFileStorageConfig() (FileStorageConfig, error) {
if e := a.ready(); e != nil {
return FileStorageConfig{}, e
}
return a.currentFileStorage(), nil
}
2026-08-15 17:18:00 +08:00
// SaveFileStorageConfig 管理员id=1保存全局配置直接写 API 立即全员生效。
2026-08-14 07:52:01 +08:00
func (a *App) SaveFileStorageConfig(c FileStorageConfig) error {
if e := a.ready(); e != nil {
return e
}
if a.syncUserID() != festivalAdminID {
return errors.New("FILE_STORAGE_ADMIN_ONLY")
}
c = normalizeFileStorage(c)
if c.Mode == "server" && !strings.HasPrefix(c.BaseURL, "http") {
return errors.New("FILE_STORAGE_BAD_URL")
}
b, e := json.Marshal(c)
if e != nil {
return e
}
now := nowRFC()
2026-08-15 17:18:00 +08:00
if e := a.apiPutSettingWithStepUp(fileStorageKey, string(b), now, true); e != nil {
return e
2026-08-14 07:52:01 +08:00
}
if e := a.store.SetMeta(fileStorageKey, string(b)); e != nil {
return e
}
_ = a.store.SetMeta(fileStorageAtKey, now)
a.invalidateFileStorageCache()
a.store.Log("info", "系统", "文件存储配置已保存", "mode="+c.Mode)
return nil
}
2026-08-15 17:18:00 +08:00
// TestFileStorage 探测服务器连通性GET {baseURL}/healthz
2026-08-14 07:52:01 +08:00
func (a *App) TestFileStorage(c FileStorageConfig) error {
if e := a.ready(); e != nil {
return e
}
c = normalizeFileStorage(c)
if !strings.HasPrefix(c.BaseURL, "http") {
return errors.New("FILE_STORAGE_BAD_URL")
}
client := &http.Client{Timeout: 5 * time.Second}
resp, e := client.Get(c.BaseURL + "/healthz")
if e != nil {
return errors.New("FILE_STORAGE_UNREACHABLE")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New("FILE_STORAGE_UNREACHABLE")
}
return nil
}