204848 commited on
Commit ·
c7ba3dd
1
Parent(s): 0c46526
feat: 增强配置文件恢复机制,支持损坏文件的备份和恢复
Browse files- .gitignore +1 -0
- internal/config/config_test.go +108 -0
- internal/config/store.go +23 -1
- internal/config/store_env_writeback.go +57 -0
.gitignore
CHANGED
|
@@ -73,3 +73,4 @@ deepseek2api旧版/
|
|
| 73 |
chat.deepseek.com.har.txt
|
| 74 |
chat.deepseek.com2.har.txt
|
| 75 |
chat.deepseek.com/
|
|
|
|
|
|
| 73 |
chat.deepseek.com.har.txt
|
| 74 |
chat.deepseek.com2.har.txt
|
| 75 |
chat.deepseek.com/
|
| 76 |
+
chat.deepseek.com3.har
|
internal/config/config_test.go
CHANGED
|
@@ -4,6 +4,7 @@ import (
|
|
| 4 |
"encoding/base64"
|
| 5 |
"errors"
|
| 6 |
"os"
|
|
|
|
| 7 |
"strings"
|
| 8 |
"testing"
|
| 9 |
)
|
|
@@ -462,3 +463,110 @@ func TestAccountTestStatusIsRuntimeOnlyAndNotPersisted(t *testing.T) {
|
|
| 462 |
t.Fatalf("expected test_status to stay out of persisted config, got: %s", content)
|
| 463 |
}
|
| 464 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"encoding/base64"
|
| 5 |
"errors"
|
| 6 |
"os"
|
| 7 |
+
"path/filepath"
|
| 8 |
"strings"
|
| 9 |
"testing"
|
| 10 |
)
|
|
|
|
| 463 |
t.Fatalf("expected test_status to stay out of persisted config, got: %s", content)
|
| 464 |
}
|
| 465 |
}
|
| 466 |
+
|
| 467 |
+
func TestLoadConfigRecoversFromCorruptFileWithBackup(t *testing.T) {
|
| 468 |
+
dir := t.TempDir()
|
| 469 |
+
path := dir + "/config.json"
|
| 470 |
+
bakPath := path + ".bak"
|
| 471 |
+
good := `{"keys":["backup-key"],"accounts":[{"email":"u@example.com","password":"p"}]}`
|
| 472 |
+
if err := os.WriteFile(bakPath, []byte(good), 0o644); err != nil {
|
| 473 |
+
t.Fatalf("write bak: %v", err)
|
| 474 |
+
}
|
| 475 |
+
if err := os.WriteFile(path, []byte(`{broken-json`), 0o644); err != nil {
|
| 476 |
+
t.Fatalf("write corrupt: %v", err)
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
cfg, err := loadConfigFromFile(path)
|
| 480 |
+
if err != nil {
|
| 481 |
+
t.Fatalf("expected recovery to avoid error, got: %v", err)
|
| 482 |
+
}
|
| 483 |
+
if len(cfg.Keys) != 1 || cfg.Keys[0] != "backup-key" {
|
| 484 |
+
t.Fatalf("expected recovery from backup key, got keys=%#v", cfg.Keys)
|
| 485 |
+
}
|
| 486 |
+
content, rerr := os.ReadFile(path)
|
| 487 |
+
if rerr != nil {
|
| 488 |
+
t.Fatalf("read recovered path: %v", rerr)
|
| 489 |
+
}
|
| 490 |
+
if !strings.Contains(string(content), "backup-key") {
|
| 491 |
+
t.Fatalf("expected path restored from backup, got: %s", content)
|
| 492 |
+
}
|
| 493 |
+
matches, _ := filepath.Glob(dir + "/config.json.corrupt.*")
|
| 494 |
+
if len(matches) == 0 {
|
| 495 |
+
t.Fatalf("expected corrupt snapshot to be preserved")
|
| 496 |
+
}
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
func TestLoadConfigResetsCorruptFileWithoutBackup(t *testing.T) {
|
| 500 |
+
dir := t.TempDir()
|
| 501 |
+
path := dir + "/config.json"
|
| 502 |
+
if err := os.WriteFile(path, []byte(`{broken`), 0o644); err != nil {
|
| 503 |
+
t.Fatalf("write corrupt: %v", err)
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
cfg, err := loadConfigFromFile(path)
|
| 507 |
+
if err != nil {
|
| 508 |
+
t.Fatalf("expected reset to avoid error, got: %v", err)
|
| 509 |
+
}
|
| 510 |
+
if len(cfg.Keys) != 0 || len(cfg.Accounts) != 0 {
|
| 511 |
+
t.Fatalf("expected empty config after reset, got keys=%d accounts=%d", len(cfg.Keys), len(cfg.Accounts))
|
| 512 |
+
}
|
| 513 |
+
content, rerr := os.ReadFile(path)
|
| 514 |
+
if rerr != nil {
|
| 515 |
+
t.Fatalf("read reset path: %v", rerr)
|
| 516 |
+
}
|
| 517 |
+
if strings.TrimSpace(string(content)) != "{}" {
|
| 518 |
+
t.Fatalf("expected path reset to {}, got: %s", content)
|
| 519 |
+
}
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
func TestLoadConfigResetsCorruptFileWithCorruptBackup(t *testing.T) {
|
| 523 |
+
dir := t.TempDir()
|
| 524 |
+
path := dir + "/config.json"
|
| 525 |
+
bakPath := path + ".bak"
|
| 526 |
+
if err := os.WriteFile(bakPath, []byte(`{also-broken`), 0o644); err != nil {
|
| 527 |
+
t.Fatalf("write corrupt bak: %v", err)
|
| 528 |
+
}
|
| 529 |
+
if err := os.WriteFile(path, []byte(`{broken`), 0o644); err != nil {
|
| 530 |
+
t.Fatalf("write corrupt: %v", err)
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
cfg, err := loadConfigFromFile(path)
|
| 534 |
+
if err != nil {
|
| 535 |
+
t.Fatalf("expected reset to avoid error, got: %v", err)
|
| 536 |
+
}
|
| 537 |
+
if len(cfg.Keys) != 0 {
|
| 538 |
+
t.Fatalf("expected empty config after reset, got keys=%d", len(cfg.Keys))
|
| 539 |
+
}
|
| 540 |
+
content, rerr := os.ReadFile(path)
|
| 541 |
+
if rerr != nil {
|
| 542 |
+
t.Fatalf("read reset path: %v", rerr)
|
| 543 |
+
}
|
| 544 |
+
if strings.TrimSpace(string(content)) != "{}" {
|
| 545 |
+
t.Fatalf("expected path reset to {}, got: %s", content)
|
| 546 |
+
}
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
func TestLoadConfigRefreshesBackupOnSuccessfulLoad(t *testing.T) {
|
| 550 |
+
dir := t.TempDir()
|
| 551 |
+
path := dir + "/config.json"
|
| 552 |
+
bakPath := path + ".bak"
|
| 553 |
+
good := `{"keys":["live-key"],"accounts":[]}`
|
| 554 |
+
if err := os.WriteFile(path, []byte(good), 0o644); err != nil {
|
| 555 |
+
t.Fatalf("write config: %v", err)
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
cfg, err := loadConfigFromFile(path)
|
| 559 |
+
if err != nil {
|
| 560 |
+
t.Fatalf("unexpected error: %v", err)
|
| 561 |
+
}
|
| 562 |
+
if len(cfg.Keys) != 1 || cfg.Keys[0] != "live-key" {
|
| 563 |
+
t.Fatalf("unexpected keys: %#v", cfg.Keys)
|
| 564 |
+
}
|
| 565 |
+
bak, rerr := os.ReadFile(bakPath)
|
| 566 |
+
if rerr != nil {
|
| 567 |
+
t.Fatalf("expected backup to be created: %v", rerr)
|
| 568 |
+
}
|
| 569 |
+
if !strings.Contains(string(bak), "live-key") {
|
| 570 |
+
t.Fatalf("expected backup to mirror live config, got: %s", bak)
|
| 571 |
+
}
|
| 572 |
+
}
|
internal/config/store.go
CHANGED
|
@@ -130,7 +130,22 @@ func loadConfigFromFile(path string) (Config, error) {
|
|
| 130 |
}
|
| 131 |
var cfg Config
|
| 132 |
if err := json.Unmarshal(content, &cfg); err != nil {
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
}
|
| 135 |
cfg.NormalizeCredentials()
|
| 136 |
cfg.DropInvalidAccounts()
|
|
@@ -139,6 +154,13 @@ func loadConfigFromFile(path string) (Config, error) {
|
|
| 139 |
_ = overwriteFile(path, b, 0o644)
|
| 140 |
}
|
| 141 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
return cfg, nil
|
| 143 |
}
|
| 144 |
|
|
|
|
| 130 |
}
|
| 131 |
var cfg Config
|
| 132 |
if err := json.Unmarshal(content, &cfg); err != nil {
|
| 133 |
+
// 配置文件损坏:保留损坏文件、尝试从 .bak 恢复、恢复失败则重置为 {}。
|
| 134 |
+
// 不返回错误,避免单点配置损坏导致整个服务启动失败(例如
|
| 135 |
+
// HuggingFace Space 重启瞬间 config.json 被截断成半截 JSON)。
|
| 136 |
+
Logger.Error("[config] config file is corrupt, attempting recovery", "path", path, "error", err)
|
| 137 |
+
if !recoverCorruptConfig(path, content) {
|
| 138 |
+
return Config{}, nil
|
| 139 |
+
}
|
| 140 |
+
content, err = os.ReadFile(path)
|
| 141 |
+
if err != nil {
|
| 142 |
+
Logger.Error("[config] re-read after recovery failed, starting with empty config", "path", path, "error", err)
|
| 143 |
+
return Config{}, nil
|
| 144 |
+
}
|
| 145 |
+
if err := json.Unmarshal(content, &cfg); err != nil {
|
| 146 |
+
Logger.Error("[config] recovered config still invalid, starting with empty config", "path", path, "error", err)
|
| 147 |
+
return Config{}, nil
|
| 148 |
+
}
|
| 149 |
}
|
| 150 |
cfg.NormalizeCredentials()
|
| 151 |
cfg.DropInvalidAccounts()
|
|
|
|
| 154 |
_ = overwriteFile(path, b, 0o644)
|
| 155 |
}
|
| 156 |
}
|
| 157 |
+
// 加载成功后刷新可用备份(仅当当前文件合法时才覆盖备份,避免用损坏
|
| 158 |
+
// 文件覆盖已知好备份)。Vercel 文件系统为临时/只读,跳过。
|
| 159 |
+
if !IsVercel() {
|
| 160 |
+
if backupErr := backupConfigFile(path, content); backupErr != nil {
|
| 161 |
+
Logger.Warn("[config] failed to refresh config backup", "path", path, "error", backupErr)
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
return cfg, nil
|
| 165 |
}
|
| 166 |
|
internal/config/store_env_writeback.go
CHANGED
|
@@ -6,6 +6,7 @@ import (
|
|
| 6 |
"os"
|
| 7 |
"path/filepath"
|
| 8 |
"strings"
|
|
|
|
| 9 |
)
|
| 10 |
|
| 11 |
func envWritebackEnabled() bool {
|
|
@@ -96,3 +97,59 @@ func writeConfigBytes(path string, b []byte) error {
|
|
| 96 |
}
|
| 97 |
return nil
|
| 98 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
"os"
|
| 7 |
"path/filepath"
|
| 8 |
"strings"
|
| 9 |
+
"time"
|
| 10 |
)
|
| 11 |
|
| 12 |
func envWritebackEnabled() bool {
|
|
|
|
| 97 |
}
|
| 98 |
return nil
|
| 99 |
}
|
| 100 |
+
|
| 101 |
+
// backupConfigFile 把当前合法的配置内容快照到 <path>.bak。仅在主配置解析
|
| 102 |
+
// 成功后调用,确保 .bak 永远是已知合法的副本,供 recoverCorruptConfig 使用。
|
| 103 |
+
func backupConfigFile(path string, content []byte) error {
|
| 104 |
+
if len(content) == 0 {
|
| 105 |
+
return nil
|
| 106 |
+
}
|
| 107 |
+
return overwriteFile(path+".bak", content, 0o644)
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// resetConfigToEmpty 把 path 重置为 {},用于损坏文件无可恢复备份时的兜底。
|
| 111 |
+
func resetConfigToEmpty(path string) {
|
| 112 |
+
if werr := writeConfigBytes(path, []byte("{}")); werr != nil {
|
| 113 |
+
Logger.Warn("[config] failed to reset config to empty", "path", path, "error", werr)
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
// recoverCorruptConfig 处理主配置文件解析失败的情况:
|
| 118 |
+
// 1. 将损坏文件另存为 <path>.corrupt.<unix> 便于排查
|
| 119 |
+
// 2. 尝试用 <path>.bak 恢复;备份不存在或也损坏则把 path 重置为 {}
|
| 120 |
+
//
|
| 121 |
+
// 返回 true 表示 path 现在指向一个合法的(可能是空的)配置文件。这样避免
|
| 122 |
+
// 单点配置损坏导致整个服务启动失败(例如 HuggingFace Space 重启瞬间
|
| 123 |
+
// config.json 被截断成半截 JSON)。
|
| 124 |
+
func recoverCorruptConfig(path string, corruptContent []byte) bool {
|
| 125 |
+
corruptPath := fmt.Sprintf("%s.corrupt.%d", path, time.Now().Unix())
|
| 126 |
+
if werr := overwriteFile(corruptPath, corruptContent, 0o644); werr != nil {
|
| 127 |
+
Logger.Warn("[config] failed to preserve corrupt config for inspection", "path", corruptPath, "error", werr)
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
bakPath := path + ".bak"
|
| 131 |
+
bakContent, rerr := os.ReadFile(bakPath)
|
| 132 |
+
if rerr != nil {
|
| 133 |
+
Logger.Warn("[config] no backup available, resetting config to empty", "path", path, "bak_path", bakPath, "error", rerr)
|
| 134 |
+
resetConfigToEmpty(path)
|
| 135 |
+
return true
|
| 136 |
+
}
|
| 137 |
+
if len(strings.TrimSpace(string(bakContent))) == 0 {
|
| 138 |
+
Logger.Warn("[config] backup is empty, resetting config to empty", "path", path, "bak_path", bakPath)
|
| 139 |
+
resetConfigToEmpty(path)
|
| 140 |
+
return true
|
| 141 |
+
}
|
| 142 |
+
var probe Config
|
| 143 |
+
if perr := json.Unmarshal(bakContent, &probe); perr != nil {
|
| 144 |
+
Logger.Warn("[config] backup is also corrupt, resetting config to empty", "path", path, "bak_path", bakPath, "error", perr)
|
| 145 |
+
resetConfigToEmpty(path)
|
| 146 |
+
return true
|
| 147 |
+
}
|
| 148 |
+
if werr := writeConfigBytes(path, bakContent); werr != nil {
|
| 149 |
+
Logger.Error("[config] failed to restore config from backup, resetting to empty", "path", path, "bak_path", bakPath, "error", werr)
|
| 150 |
+
resetConfigToEmpty(path)
|
| 151 |
+
return true
|
| 152 |
+
}
|
| 153 |
+
Logger.Info("[config] recovered config from backup", "path", path, "bak_path", bakPath)
|
| 154 |
+
return true
|
| 155 |
+
}
|