Files
Scoreboard/console/main/datadir.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

113 lines
4.6 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 fs = require('fs')
const path = require('path')
// 数据目录必须落在安装目录之外:electron-builder 的卸载器结尾是 `RMDir /r $INSTDIR`,
// 而覆盖安装会先静默跑一遍旧版卸载器 → 装在安装目录里的 data/ 会被整体删掉。
// 也不能放 %APPDATA%\<AppName>(卸载器同样会删),所以默认落在 %LOCALAPPDATA%\ScoreboardConsole\data。
const LOCAL_BASE = 'ScoreboardConsole'
const DATA_SUBDIR = 'data'
// 解析最终数据目录。installDir 是安装目录(打包后 = exe 所在目录),
// 相对路径按安装目录解析,现场写 `data`、`D:\scoreboard-data` 都能用。
// passToServer=false 表示不给服务端加 -data,沿用服务端自身默认(开发态)。
function resolveDataDir({ values, installDir, env, isPackaged }) {
const base = path.resolve(installDir || '.')
const configured = values && values.server ? values.server.dataDir : ''
if (typeof configured === 'string' && configured.trim() !== '') {
const value = configured.trim()
return {
dir: path.isAbsolute(value) ? path.resolve(value) : path.resolve(base, value),
source: 'config',
passToServer: true
}
}
// 开发态不接管:继续用服务端默认(工作目录下的 data),免得仓库里的 data/ 被顶掉
if (!isPackaged) {
return { dir: path.join(base, DATA_SUBDIR), source: 'default', passToServer: false }
}
const localAppData = env ? env.LOCALAPPDATA : ''
if (!localAppData) {
// 拿不到 %LOCALAPPDATA% 时退回老位置:这次覆盖安装仍有丢数据风险,
// 但总好过把数据指向一个猜出来的路径
return { dir: path.join(base, DATA_SUBDIR), source: 'fallback', passToServer: false }
}
return {
dir: path.join(localAppData, LOCAL_BASE, DATA_SUBDIR),
source: 'localappdata',
passToServer: true
}
}
// 判断 dir 是否落在 base 之内(含 base 本身):用于提醒「数据还在安装目录里」,
// 这种配置在覆盖安装时会被卸载器连目录一起删掉
function isInside(base, dir) {
const rel = path.relative(path.resolve(base), path.resolve(dir))
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))
}
// missing = 不存在,empty = 存在但空,has-data = 有内容
// 读不出来时返回 error,由调用方按「可能有数据」处理,宁可不迁移
function dirState(dir) {
let entries
try {
entries = fs.readdirSync(dir)
} catch (err) {
return err.code === 'ENOENT' ? 'missing' : 'error'
}
return entries.length > 0 ? 'has-data' : 'empty'
}
// 一次性迁移:只在「目标为空且旧目录有数据」时把旧目录整体改名过去。
// 任何一步失败都只记日志、旧数据保持原地,绝不删除数据;
// 两边都有数据时不做任何动作,只提示现场人工确认。
function migrateLegacyData({ targetDir, legacyDir, log }) {
const notify = typeof log === 'function' ? log : () => {}
if (!targetDir || !legacyDir) return { migrated: false, reason: 'invalid' }
if (path.resolve(targetDir) === path.resolve(legacyDir)) return { migrated: false, reason: 'same-dir' }
const target = dirState(targetDir)
const legacy = dirState(legacyDir)
if (target === 'error') {
notify(`警告:读不到数据目录 ${targetDir},跳过旧数据迁移;旧数据仍在 ${legacyDir}`)
return { migrated: false, reason: 'target-unreadable' }
}
if (target === 'has-data') {
if (legacy === 'has-data') {
notify(
`警告:旧数据目录 ${legacyDir} 与新数据目录 ${targetDir} 都有数据,未做迁移。` +
'请人工确认要保留哪一份(两份都不会被自动删除)'
)
return { migrated: false, reason: 'both' }
}
return { migrated: false, reason: 'target-has-data' }
}
if (legacy !== 'has-data') return { migrated: false, reason: 'no-legacy' }
try {
// Windows 上 rename 到已存在的目录会失败,先摘掉这个空目录(空目录里没有数据)
if (target === 'empty') fs.rmdirSync(targetDir)
fs.mkdirSync(path.dirname(targetDir), { recursive: true })
fs.renameSync(legacyDir, targetDir)
} catch (err) {
notify(
`警告:迁移旧数据失败(${err.message}),旧数据仍保留在 ${legacyDir};` +
`本次服务端将使用 ${targetDir},请人工把旧目录内容拷过去`
)
return { migrated: false, reason: 'failed' }
}
notify(`已把旧数据从 ${legacyDir} 迁移到 ${targetDir}`)
return { migrated: true, reason: 'moved' }
}
module.exports = { resolveDataDir, migrateLegacyData, isInside }