Files
Scoreboard/console/main/datadir.test.js
T
AskaEth 005b51b2ec fix: 数据目录移出安装目录,修掉「覆盖安装/卸载即丢数据」
根因:数据目录是 <安装目录>\data(config.yaml 的 dataDir 相对服务端工作目录),
而 electron-builder 的卸载器结尾执行 RMDir /r $INSTDIR,且覆盖安装会先跑旧卸载器
(installSection.nsh:52 的 uninstallOldVersion)→ 赛事库/规则/用户/审计/上传图片
连同安装目录被整个删除,再装新版。卸载一次同样如此。

- 服务端新增 -data <dir>(主命令、backup、passwd 三处),非空时覆盖 config.yaml 的
  dataDir;不传时行为完全不变(兼容便携/手工运行)
- Console 把数据指到当前用户目录 %LOCALAPPDATA%\ScoreboardConsole\data(卸载器不会碰
  这里;没有用 app.getPath('userData'),因为 %APPDATA%\<AppName> 正是被删的那个)
- 一次性迁移:目标为空且 <安装目录>\data 有数据时整体改名过去;两边都有数据时不动作
  只记警告;任何失败都保留原地数据、绝不删除
- passwd 也带 -data(否则重置的是安装目录下那个空库里的账号)
- deploy/backup.bat 指向同一数据目录
- 文档:部署指南新增「数据放在哪里(升级/卸载都不会删)」+ 备份/恢复/迁移路径改写,
  上线检查清单与使用手册同步

自检:服务端 12 包全绿;console 语法 14 文件 + datadir 单测 11/11;backup.bat 保持纯 ASCII。
2026-09-10 09:17:56 +08:00

192 lines
7.0 KiB
JavaScript
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.
'use strict'
const assert = require('node:assert/strict')
const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
const test = require('node:test')
const { resolveDataDir, migrateLegacyData, isInside } = require('./datadir')
// 用绝对路径字符串拼测试目录,避免依赖运行平台的盘符写法
const INSTALL = path.resolve('/scoreboard-install')
const LOCAL = path.resolve('/localappdata')
function makeRoot(t) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'scoreboard-datadir-'))
t.after(() => fs.rmSync(root, { recursive: true, force: true }))
return root
}
function writeFile(file, text) {
fs.mkdirSync(path.dirname(file), { recursive: true })
fs.writeFileSync(file, text)
}
test('打包态默认数据目录落在 %LOCALAPPDATA%\\ScoreboardConsole\\data', () => {
const info = resolveDataDir({
values: { server: { dataDir: '' } },
installDir: INSTALL,
env: { LOCALAPPDATA: LOCAL },
isPackaged: true
})
assert.equal(info.dir, path.join(LOCAL, 'ScoreboardConsole', 'data'))
assert.equal(info.source, 'localappdata')
assert.equal(info.passToServer, true)
assert.equal(isInside(INSTALL, info.dir), false)
})
test('开发态不接管数据目录,也不给服务端加 -data', () => {
const info = resolveDataDir({
values: { server: { dataDir: '' } },
installDir: INSTALL,
env: { LOCALAPPDATA: LOCAL },
isPackaged: false
})
assert.equal(info.dir, path.join(INSTALL, 'data'))
assert.equal(info.passToServer, false)
})
test('配置了绝对路径就按配置值走', () => {
const custom = path.resolve('/scoreboard-data')
const info = resolveDataDir({
values: { server: { dataDir: custom } },
installDir: INSTALL,
env: { LOCALAPPDATA: LOCAL },
isPackaged: true
})
assert.equal(info.dir, custom)
assert.equal(info.source, 'config')
assert.equal(info.passToServer, true)
})
test('配置了相对路径按安装目录解析', () => {
const info = resolveDataDir({
values: { server: { dataDir: 'data' } },
installDir: INSTALL,
env: { LOCALAPPDATA: LOCAL },
isPackaged: true
})
assert.equal(info.dir, path.join(INSTALL, 'data'))
assert.equal(info.passToServer, true)
assert.equal(isInside(INSTALL, info.dir), true)
})
test('缺 %LOCALAPPDATA% 时退回安装目录,不下发 -data', () => {
const info = resolveDataDir({
values: { server: {} },
installDir: INSTALL,
env: {},
isPackaged: true
})
assert.equal(info.dir, path.join(INSTALL, 'data'))
assert.equal(info.source, 'fallback')
assert.equal(info.passToServer, false)
})
test('目标为空且旧目录有数据 → 整体迁移', (t) => {
const root = makeRoot(t)
const legacyDir = path.join(root, 'install', 'data')
const targetDir = path.join(root, 'localappdata', 'ScoreboardConsole', 'data')
writeFile(path.join(legacyDir, 'system.db'), 'db')
writeFile(path.join(legacyDir, 'event_projs', 'default', 'event.db'), 'event')
const lines = []
const result = migrateLegacyData({ targetDir, legacyDir, log: (line) => lines.push(line) })
assert.equal(result.migrated, true)
assert.equal(result.reason, 'moved')
assert.equal(fs.existsSync(legacyDir), false)
assert.equal(fs.readFileSync(path.join(targetDir, 'system.db'), 'utf8'), 'db')
assert.equal(fs.existsSync(path.join(targetDir, 'event_projs', 'default', 'event.db')), true)
assert.match(lines.join('\n'), /已把旧数据从 .* 迁移到 /)
})
test('目标目录已存在但是空目录 → 照样迁移', (t) => {
const root = makeRoot(t)
const legacyDir = path.join(root, 'install', 'data')
const targetDir = path.join(root, 'localappdata', 'ScoreboardConsole', 'data')
writeFile(path.join(legacyDir, 'system.db'), 'db')
fs.mkdirSync(targetDir, { recursive: true })
const result = migrateLegacyData({ targetDir, legacyDir, log: () => {} })
assert.equal(result.migrated, true)
assert.equal(fs.readFileSync(path.join(targetDir, 'system.db'), 'utf8'), 'db')
assert.equal(fs.existsSync(legacyDir), false)
})
test('两边都有数据 → 不动任何一边,只记日志', (t) => {
const root = makeRoot(t)
const legacyDir = path.join(root, 'install', 'data')
const targetDir = path.join(root, 'localappdata', 'ScoreboardConsole', 'data')
writeFile(path.join(legacyDir, 'system.db'), 'old')
writeFile(path.join(targetDir, 'system.db'), 'new')
const lines = []
const result = migrateLegacyData({ targetDir, legacyDir, log: (line) => lines.push(line) })
assert.equal(result.migrated, false)
assert.equal(result.reason, 'both')
assert.equal(fs.readFileSync(path.join(legacyDir, 'system.db'), 'utf8'), 'old')
assert.equal(fs.readFileSync(path.join(targetDir, 'system.db'), 'utf8'), 'new')
assert.match(lines.join('\n'), /都有数据/)
})
test('旧目录没有数据 / 目标已有数据 / 两边同路径 → 都不动作', (t) => {
const root = makeRoot(t)
const emptyLegacy = path.join(root, 'a', 'data')
fs.mkdirSync(emptyLegacy, { recursive: true })
assert.equal(
migrateLegacyData({ targetDir: path.join(root, 'b', 'data'), legacyDir: emptyLegacy, log: () => {} }).reason,
'no-legacy'
)
assert.equal(
migrateLegacyData({ targetDir: path.join(root, 'c', 'data'), legacyDir: path.join(root, 'd', 'data'), log: () => {} }).reason,
'no-legacy'
)
const legacyDir = path.join(root, 'e', 'data')
const targetDir = path.join(root, 'f', 'data')
writeFile(path.join(targetDir, 'system.db'), 'new')
assert.equal(migrateLegacyData({ targetDir, legacyDir, log: () => {} }).reason, 'target-has-data')
assert.equal(migrateLegacyData({ targetDir, legacyDir: targetDir, log: () => {} }).reason, 'same-dir')
})
test('迁移失败时旧数据原地不动', (t) => {
const root = makeRoot(t)
// 目标目录嵌在旧目录内部,rename 必然失败(EINVAL)
const legacyDir = path.join(root, 'install', 'data')
const targetDir = path.join(legacyDir, 'nested')
writeFile(path.join(legacyDir, 'system.db'), 'db')
const lines = []
const result = migrateLegacyData({ targetDir, legacyDir, log: (line) => lines.push(line) })
assert.equal(result.migrated, false)
assert.equal(result.reason, 'failed')
assert.equal(fs.readFileSync(path.join(legacyDir, 'system.db'), 'utf8'), 'db')
assert.match(lines.join('\n'), /旧数据仍保留在/)
})
test('目标路径读不出来时按「可能有数据」处理,不做迁移', (t) => {
const root = makeRoot(t)
const legacyDir = path.join(root, 'install', 'data')
writeFile(path.join(legacyDir, 'system.db'), 'db')
// 目标目录的上级是个文件,readdir 会报 ENOTDIR
const blocker = path.join(root, 'blocker')
writeFile(blocker, 'not a dir')
const lines = []
const result = migrateLegacyData({
targetDir: path.join(blocker, 'data'),
legacyDir,
log: (line) => lines.push(line)
})
assert.equal(result.migrated, false)
assert.equal(result.reason, 'target-unreadable')
assert.equal(fs.readFileSync(path.join(legacyDir, 'system.db'), 'utf8'), 'db')
assert.match(lines.join('\n'), /跳过旧数据迁移/)
})