Files
code-utils/filestorage.go
2026-08-15 17:18:00 +08:00

153 lines
4.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
// filestorage.go 全局「文件存储方式」配置:由管理员(云端账号 id=1在设置页配置
// 权威数据经 nl-pms-api settings全局键 file_storage
// 保存时直接写 API 立即生效;各端上传图片时实时拉取该配置(短 TTL 缓存),
// 拉取失败(离线/未配置同步)回退本地缓存副本(由同步循环下发,见 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
}
// defaultFileStorage 未配置时的默认:服务器模式,地址与打包同步 API 基址一致。
func (a *App) defaultFileStorage() FileStorageConfig {
return normalizeFileStorage(FileStorageConfig{
Mode: "server",
BaseURL: a.apiBaseURL(),
})
}
func (a *App) fileStorageConfig() FileStorageConfig {
var c FileStorageConfig
if raw := a.store.Meta(fileStorageKey); raw != "" {
_ = json.Unmarshal([]byte(raw), &c)
return normalizeFileStorage(c)
}
return a.defaultFileStorage()
}
// fetchRemoteFileStorage 从 API 读权威配置。第二个返回值表示"远端可用且结果权威"
// (无行记录时回落默认服务器配置);成功时顺带刷新本地缓存副本,供离线兜底。
func (a *App) fetchRemoteFileStorage() (FileStorageConfig, bool) {
if a.syncUserID() <= 0 || a.store.Meta("sync_access_token") == "" || a.apiBaseURL() == "" {
return FileStorageConfig{}, false
}
val, at, ok, e := a.apiGetGlobalSetting(fileStorageKey)
if e != nil {
return FileStorageConfig{}, false
}
if !ok {
return a.defaultFileStorage(), true
}
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
}
// SaveFileStorageConfig 管理员id=1保存全局配置直接写 API 立即全员生效。
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()
if e := a.apiPutSettingWithStepUp(fileStorageKey, string(b), now, true); e != nil {
return e
}
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
}
// TestFileStorage 探测服务器连通性GET {baseURL}/healthz
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
}