diff --git a/backend/repository/db.go b/backend/repository/db.go index 8ec0c4b..1524f62 100644 --- a/backend/repository/db.go +++ b/backend/repository/db.go @@ -539,6 +539,35 @@ func (d *JsonDB) migrateSchema() bool { return true } +// migrateLegacyDataFile 首次从历史命名(opsxloki.json / lokideploy.json)加载后, +// 立即升级到 lokistack.json:数据原子写入新文件,旧文件改名 .migrated-*.bak 留档。 +// 不迁移的话回退链会让旧文件名(旧品牌残留)一直被沿用下去。 +func (d *JsonDB) migrateLegacyDataFile() { + const newName = "lokistack.json" + if filepath.Base(d.dataFile) == newName { + return + } + legacyPath := d.dataFile + newPath := filepath.Join(filepath.Dir(legacyPath), newName) + if _, err := os.Stat(newPath); err == nil { + // 新旧并存属异常态:不覆盖任何一边,沿用旧路径并留日志交人工判断 + log.Printf("[db] 检测到 %s 与 %s 并存,沿用 %s(请人工确认)", newPath, legacyPath, legacyPath) + return + } + d.dataFile = newPath + if err := d.doSave(); err != nil { + d.dataFile = legacyPath // 写新文件失败:退回旧路径,下次启动再试 + log.Printf("[ERROR] [db] 迁移数据文件到 %s 失败(%v),继续使用 %s", newPath, err, legacyPath) + return + } + backup := fmt.Sprintf("%s.migrated-%s.bak", legacyPath, time.Now().Format("20060102-150405")) + if err := os.Rename(legacyPath, backup); err != nil { + log.Printf("[db] 数据已写入 %s,但旧文件 %s 改名留档失败(%v)", newPath, legacyPath, err) + return + } + log.Printf("[db] 数据文件已从 %s 升级为 %s(原文件留档 %s)", legacyPath, newPath, backup) +} + // loadFromDisk 读取并解析数据文件,重建 ID 序列;文件损坏时备份留档绝不覆盖。 func (d *JsonDB) loadFromDisk() { if data, err := os.ReadFile(d.dataFile); err == nil { @@ -681,6 +710,7 @@ func InitDB() error { } d.loadFromDisk() + d.migrateLegacyDataFile() migratedLogAccess := migrateLogAccessToGroups(d) // 结构版本迁移(审计表、采集项等新字段的兼容处理) migratedSchema := d.migrateSchema() diff --git a/backend/repository/db_migrate_test.go b/backend/repository/db_migrate_test.go new file mode 100644 index 0000000..091aed9 --- /dev/null +++ b/backend/repository/db_migrate_test.go @@ -0,0 +1,57 @@ +package repository + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// TestMigrateLegacyDataFile 首次从历史命名(opsxloki.json)加载后必须升级到 lokistack.json: +// 新文件出现且内容完整,旧文件改名留档(不删数据)。 +func TestMigrateLegacyDataFile(t *testing.T) { + dir := t.TempDir() + legacy := filepath.Join(dir, "opsxloki.json") + seed := `{"schemaVersion":10,"users":[{"id":1,"username":"admin"}],"clusters":[],"hosts":[],"storages":[],"deployments":[],"collectors":[],"logs":[],"settings":[],"logSources":[],"alertRules":[],"alertDeliveries":[],"notifyChannels":[],"logPolicyGroups":[],"auditLogs":[],"dailyVolumeSnapshots":[]}` + if err := os.WriteFile(legacy, []byte(seed), 0o600); err != nil { + t.Fatal(err) + } + + d := &JsonDB{dataFile: legacy} + d.loadFromDisk() + d.migrateLegacyDataFile() + + // 新文件存在且包含原有数据 + content, err := os.ReadFile(filepath.Join(dir, "lokistack.json")) + if err != nil { + t.Fatalf("未生成 lokistack.json: %v", err) + } + if !strings.Contains(string(content), `"admin"`) { + t.Fatalf("迁移后数据不完整: %s", content[:min(len(content), 120)]) + } + // 旧文件被改名留档,原内容不丢 + entries, _ := os.ReadDir(dir) + backedUp := false + for _, e := range entries { + if strings.HasPrefix(e.Name(), "opsxloki.json.migrated-") { + kept, _ := os.ReadFile(filepath.Join(dir, e.Name())) + if string(kept) == seed { + backedUp = true + } + } + } + if !backedUp { + t.Fatalf("旧文件未留档或内容被改动: %v", entries) + } + // dataFile 已切到新路径 + if filepath.Base(d.dataFile) != "lokistack.json" { + t.Fatalf("dataFile 未切换: %s", d.dataFile) + } +} + +func min(a, b int) int { + if a < b { + return a + } + return b +}