mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
fix(config): store config.json under the persisted data dir (#86)
CONFIG_PATH resolved to ROOT_DIR/config.json (/app/config.json in Docker), but only DATA_DIR (/app/data) is the mounted volume — every other artifact (DB, cookies, logs, avatars) already lives under DATA_DIR. So on first run the default config was written into the ephemeral image layer (never appearing in the volume), and a manually-placed data/config.json was ignored because the bot read/wrote the root path. - Move CONFIG_PATH to DATA_DIR/config.json so it lands in the volume and manual edits take effect. - Add migrateLegacyConfig(): one-time move of an existing root-level config.json into the data dir, so existing local installs keep their settings (no silent reset). - Tests for first-run persistence + the three migration cases. - README directory tree updated to data/config.json. Fixes #86 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
bea2f92508
commit
dd6affca6d
4 files changed
+102
-10
No files matched your search
@@ -383,11 +383,11 @@ teamspeak-music-bot/
|
|||||||
│ └── docker/ # Docker 部署文件
|
│ └── docker/ # Docker 部署文件
|
||||||
│ ├── Dockerfile
|
│ ├── Dockerfile
|
||||||
│ └── docker-compose.yml
|
│ └── docker-compose.yml
|
||||||
├── data/ # 运行时数据(自动创建,不上传)
|
└── data/ # 运行时数据(自动创建,不上传)
|
||||||
│ ├── tsmusicbot.db # SQLite 数据库
|
├── config.json # 配置文件(首次运行自动生成,可手动编辑)
|
||||||
│ ├── cookies/ # 登录 Cookie
|
├── tsmusicbot.db # SQLite 数据库
|
||||||
│ └── logs/ # 日志文件
|
├── cookies/ # 登录 Cookie
|
||||||
└── config.json # 配置文件(首次运行自动生成,不上传)
|
└── logs/ # 日志文件
|
||||||
```
|
```
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|||||||
+55
-2
@@ -1,8 +1,8 @@
|
|||||||
import { describe, it, expect, afterEach } from "vitest";
|
import { describe, it, expect, afterEach } from "vitest";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
import { mkdtempSync, rmSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { getDefaultConfig, loadConfig, saveConfig } from "./config.js";
|
import { getDefaultConfig, loadConfig, saveConfig, migrateLegacyConfig } from "./config.js";
|
||||||
|
|
||||||
describe("config", () => {
|
describe("config", () => {
|
||||||
const dirs: string[] = [];
|
const dirs: string[] = [];
|
||||||
@@ -51,4 +51,57 @@ describe("config", () => {
|
|||||||
expect(loaded.commandPrefix).toBe("!");
|
expect(loaded.commandPrefix).toBe("!");
|
||||||
expect(loaded.autoPauseOnEmpty).toBe(true);
|
expect(loaded.autoPauseOnEmpty).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- #86: config.json must live under (and be created in) the persisted data dir ---
|
||||||
|
|
||||||
|
it("first run writes config.json into the data dir and reads it back", () => {
|
||||||
|
const root = makeTmpDir();
|
||||||
|
const dataDir = join(root, "data");
|
||||||
|
const configPath = join(dataDir, "config.json"); // mirrors index.ts CONFIG_PATH
|
||||||
|
|
||||||
|
// Boot sequence: load (missing -> defaults) then save.
|
||||||
|
const config = loadConfig(configPath);
|
||||||
|
saveConfig(configPath, config);
|
||||||
|
|
||||||
|
expect(existsSync(configPath)).toBe(true);
|
||||||
|
// A subsequent hand-edited file under the SAME persisted path is honored.
|
||||||
|
writeFileSync(configPath, JSON.stringify({ webPort: 9999 }), "utf-8");
|
||||||
|
expect(loadConfig(configPath).webPort).toBe(9999);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migrates a legacy root config into the data dir, preserving values", () => {
|
||||||
|
const root = makeTmpDir();
|
||||||
|
const legacyPath = join(root, "config.json");
|
||||||
|
const newPath = join(root, "data", "config.json");
|
||||||
|
writeFileSync(legacyPath, JSON.stringify({ webPort: 4242, publicUrl: "http://x" }), "utf-8");
|
||||||
|
|
||||||
|
const migrated = migrateLegacyConfig(legacyPath, newPath);
|
||||||
|
|
||||||
|
expect(migrated).toBe(true);
|
||||||
|
expect(existsSync(newPath)).toBe(true);
|
||||||
|
expect(existsSync(legacyPath)).toBe(false); // legacy moved, not duplicated
|
||||||
|
const loaded = loadConfig(newPath);
|
||||||
|
expect(loaded.webPort).toBe(4242);
|
||||||
|
expect(loaded.publicUrl).toBe("http://x");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT overwrite an existing data-dir config during migration", () => {
|
||||||
|
const root = makeTmpDir();
|
||||||
|
const legacyPath = join(root, "config.json");
|
||||||
|
const newPath = join(root, "data", "config.json");
|
||||||
|
writeFileSync(legacyPath, JSON.stringify({ webPort: 1111 }), "utf-8");
|
||||||
|
saveConfig(newPath, { ...getDefaultConfig(), webPort: 2222 });
|
||||||
|
|
||||||
|
const migrated = migrateLegacyConfig(legacyPath, newPath);
|
||||||
|
|
||||||
|
expect(migrated).toBe(false); // new location wins, untouched
|
||||||
|
expect(loadConfig(newPath).webPort).toBe(2222);
|
||||||
|
expect(existsSync(legacyPath)).toBe(true); // legacy left intact when not migrated
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migration is a no-op when there is no legacy config", () => {
|
||||||
|
const root = makeTmpDir();
|
||||||
|
const migrated = migrateLegacyConfig(join(root, "config.json"), join(root, "data", "config.json"));
|
||||||
|
expect(migrated).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
+33
-1
@@ -1,4 +1,4 @@
|
|||||||
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync, rmSync } from "node:fs";
|
||||||
import { dirname } from "node:path";
|
import { dirname } from "node:path";
|
||||||
|
|
||||||
export interface BotConfig {
|
export interface BotConfig {
|
||||||
@@ -58,3 +58,35 @@ export function saveConfig(path: string, config: BotConfig): void {
|
|||||||
mkdirSync(dirname(path), { recursive: true });
|
mkdirSync(dirname(path), { recursive: true });
|
||||||
writeFileSync(path, JSON.stringify(config, null, 2), "utf-8");
|
writeFileSync(path, JSON.stringify(config, null, 2), "utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-time migration for the config location fix (#86).
|
||||||
|
*
|
||||||
|
* Older versions wrote config.json to the app/repo ROOT, which is NOT inside the
|
||||||
|
* persisted data directory (the Docker volume is mounted at data/). That meant the
|
||||||
|
* file never landed in the volume on first run and a manually-placed data/config.json
|
||||||
|
* was ignored. config.json now lives under the data dir alongside the DB/cookies/logs.
|
||||||
|
*
|
||||||
|
* If a legacy root-level config exists and the new data-dir config does not yet exist,
|
||||||
|
* move it so existing local installs keep their customized settings. Best-effort:
|
||||||
|
* any failure is swallowed and loadConfig falls back to defaults.
|
||||||
|
*
|
||||||
|
* @returns true if a legacy config was migrated, false otherwise.
|
||||||
|
*/
|
||||||
|
export function migrateLegacyConfig(legacyPath: string, newPath: string): boolean {
|
||||||
|
try {
|
||||||
|
if (legacyPath === newPath) return false;
|
||||||
|
if (existsSync(newPath)) return false; // new location already populated — leave it
|
||||||
|
if (!existsSync(legacyPath)) return false; // nothing to migrate
|
||||||
|
mkdirSync(dirname(newPath), { recursive: true });
|
||||||
|
copyFileSync(legacyPath, newPath); // copy first (works across filesystems)
|
||||||
|
try {
|
||||||
|
rmSync(legacyPath);
|
||||||
|
} catch {
|
||||||
|
/* leave the legacy file if it can't be removed; the new one wins */
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-2
@@ -1,6 +1,6 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { loadConfig, saveConfig } from "./data/config.js";
|
import { loadConfig, saveConfig, migrateLegacyConfig } from "./data/config.js";
|
||||||
import { createDatabase } from "./data/database.js";
|
import { createDatabase } from "./data/database.js";
|
||||||
import { createLogger } from "./logger.js";
|
import { createLogger } from "./logger.js";
|
||||||
import { createApiServerManager } from "./music/api-server.js";
|
import { createApiServerManager } from "./music/api-server.js";
|
||||||
@@ -16,7 +16,11 @@ import { createWebServer } from "./web/server.js";
|
|||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const ROOT_DIR = path.resolve(__dirname, "..");
|
const ROOT_DIR = path.resolve(__dirname, "..");
|
||||||
const DATA_DIR = path.join(ROOT_DIR, "data");
|
const DATA_DIR = path.join(ROOT_DIR, "data");
|
||||||
const CONFIG_PATH = path.join(ROOT_DIR, "config.json");
|
// config.json lives under the persisted data dir (the Docker volume) alongside the
|
||||||
|
// DB/cookies/logs, so it survives container restarts and manual edits take effect
|
||||||
|
// (#86). LEGACY_CONFIG_PATH is the old root-level location we migrate from once.
|
||||||
|
const CONFIG_PATH = path.join(DATA_DIR, "config.json");
|
||||||
|
const LEGACY_CONFIG_PATH = path.join(ROOT_DIR, "config.json");
|
||||||
const DB_PATH = path.join(DATA_DIR, "tsmusicbot.db");
|
const DB_PATH = path.join(DATA_DIR, "tsmusicbot.db");
|
||||||
const LOG_DIR = path.join(DATA_DIR, "logs");
|
const LOG_DIR = path.join(DATA_DIR, "logs");
|
||||||
const COOKIE_DIR = path.join(DATA_DIR, "cookies");
|
const COOKIE_DIR = path.join(DATA_DIR, "cookies");
|
||||||
@@ -24,6 +28,9 @@ const AVATAR_DIR = path.join(DATA_DIR, "avatars");
|
|||||||
const STATIC_DIR = path.join(ROOT_DIR, "web", "dist");
|
const STATIC_DIR = path.join(ROOT_DIR, "web", "dist");
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
// Migrate a pre-#86 root-level config.json into the data dir so existing
|
||||||
|
// installs keep their settings; no-op if already migrated or none exists.
|
||||||
|
migrateLegacyConfig(LEGACY_CONFIG_PATH, CONFIG_PATH);
|
||||||
const config = loadConfig(CONFIG_PATH);
|
const config = loadConfig(CONFIG_PATH);
|
||||||
saveConfig(CONFIG_PATH, config);
|
saveConfig(CONFIG_PATH, config);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user