mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 08:55:28 +08:00
feat(config): bootstrap config.json when env writeback is enabled
This commit is contained in:
@@ -79,6 +79,51 @@ func TestLoadStorePreservesFileBackedTokensForRuntime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvBackedStoreWritebackBootstrapsMissingConfigFile(t *testing.T) {
|
||||
tmp, err := os.CreateTemp(t.TempDir(), "config-*.json")
|
||||
if err != nil {
|
||||
t.Fatalf("create temp config: %v", err)
|
||||
}
|
||||
path := tmp.Name()
|
||||
_ = tmp.Close()
|
||||
_ = os.Remove(path)
|
||||
|
||||
t.Setenv("DS2API_CONFIG_JSON", `{"keys":["k1"],"accounts":[{"email":"seed@example.com","password":"p"}]}`)
|
||||
t.Setenv("CONFIG_JSON", "")
|
||||
t.Setenv("DS2API_CONFIG_PATH", path)
|
||||
t.Setenv("DS2API_ENV_WRITEBACK", "1")
|
||||
|
||||
store := LoadStore()
|
||||
if store.IsEnvBacked() {
|
||||
t.Fatalf("expected writeback bootstrap to become file-backed immediately")
|
||||
}
|
||||
if err := store.Update(func(c *Config) error {
|
||||
c.Accounts = append(c.Accounts, Account{Email: "new@example.com", Password: "p2"})
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatalf("update failed: %v", err)
|
||||
}
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read written config: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(content), "seed@example.com") {
|
||||
t.Fatalf("expected bootstrapped config to contain seed account, got: %s", content)
|
||||
}
|
||||
if !strings.Contains(string(content), "new@example.com") {
|
||||
t.Fatalf("expected persisted config to contain added account, got: %s", content)
|
||||
}
|
||||
|
||||
reloaded := LoadStore()
|
||||
if reloaded.IsEnvBacked() {
|
||||
t.Fatalf("expected reloaded store to prefer persisted config file")
|
||||
}
|
||||
accounts := reloaded.Accounts()
|
||||
if len(accounts) != 2 {
|
||||
t.Fatalf("expected 2 accounts after reload, got %d", len(accounts))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeTokenRefreshIntervalHoursDefaultsToSix(t *testing.T) {
|
||||
t.Setenv("DS2API_CONFIG_JSON", `{
|
||||
"keys":["k1"],
|
||||
|
||||
@@ -4,12 +4,19 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func envWritebackEnabled() bool {
|
||||
v := strings.ToLower(strings.TrimSpace(os.Getenv("DS2API_ENV_WRITEBACK")))
|
||||
return v == "1" || v == "true" || v == "yes" || v == "on"
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
mu sync.RWMutex
|
||||
cfg Config
|
||||
@@ -42,6 +49,24 @@ func loadConfig() (Config, bool, error) {
|
||||
cfg, err := parseConfigString(rawCfg)
|
||||
cfg.ClearAccountTokens()
|
||||
cfg.DropInvalidAccounts()
|
||||
if IsVercel() || !envWritebackEnabled() {
|
||||
return cfg, true, err
|
||||
}
|
||||
content, fileErr := os.ReadFile(ConfigPath())
|
||||
if fileErr == nil {
|
||||
var fileCfg Config
|
||||
if unmarshalErr := json.Unmarshal(content, &fileCfg); unmarshalErr == nil {
|
||||
fileCfg.DropInvalidAccounts()
|
||||
return fileCfg, false, err
|
||||
}
|
||||
}
|
||||
if errors.Is(fileErr, os.ErrNotExist) {
|
||||
if writeErr := writeConfigFile(ConfigPath(), cfg.Clone()); writeErr == nil {
|
||||
return cfg, false, err
|
||||
} else {
|
||||
Logger.Warn("[config] env writeback bootstrap failed", "error", writeErr)
|
||||
}
|
||||
}
|
||||
return cfg, true, err
|
||||
}
|
||||
|
||||
@@ -177,7 +202,7 @@ func (s *Store) Update(mutator func(*Config) error) error {
|
||||
func (s *Store) Save() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.fromEnv {
|
||||
if s.fromEnv && (IsVercel() || !envWritebackEnabled()) {
|
||||
Logger.Info("[save_config] source from env, skip write")
|
||||
return nil
|
||||
}
|
||||
@@ -187,11 +212,15 @@ func (s *Store) Save() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(s.path, b, 0o644)
|
||||
if err := writeConfigBytes(s.path, b); err != nil {
|
||||
return err
|
||||
}
|
||||
s.fromEnv = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) saveLocked() error {
|
||||
if s.fromEnv {
|
||||
if s.fromEnv && (IsVercel() || !envWritebackEnabled()) {
|
||||
Logger.Info("[save_config] source from env, skip write")
|
||||
return nil
|
||||
}
|
||||
@@ -201,7 +230,32 @@ func (s *Store) saveLocked() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(s.path, b, 0o644)
|
||||
if err := writeConfigBytes(s.path, b); err != nil {
|
||||
return err
|
||||
}
|
||||
s.fromEnv = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeConfigFile(path string, cfg Config) error {
|
||||
persistCfg := cfg.Clone()
|
||||
persistCfg.ClearAccountTokens()
|
||||
b, err := json.MarshalIndent(persistCfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeConfigBytes(path, b)
|
||||
}
|
||||
|
||||
func writeConfigBytes(path string, b []byte) error {
|
||||
dir := filepath.Dir(path)
|
||||
if dir == "." || dir == "" {
|
||||
return os.WriteFile(path, b, 0o644)
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("mkdir config dir: %w", err)
|
||||
}
|
||||
return os.WriteFile(path, b, 0o644)
|
||||
}
|
||||
|
||||
func (s *Store) IsEnvBacked() bool {
|
||||
|
||||
Reference in New Issue
Block a user