'use strict' const fs = require('fs') const path = require('path') // 数据目录必须落在安装目录之外:electron-builder 的卸载器结尾是 `RMDir /r $INSTDIR`, // 而覆盖安装会先静默跑一遍旧版卸载器 → 装在安装目录里的 data/ 会被整体删掉。 // 也不能放 %APPDATA%\(卸载器同样会删),所以默认落在 %LOCALAPPDATA%\ScoreboardConsole\data。 // 配置与规则目录(userpaths.js)用同一个基座,这里导出给它复用。 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' } } // 旧目录读不出来(EACCES / EPERM / ENOTDIR 等)与「没有旧数据」是两回事: // 前者可能是一份没搬成功的真实数据,不能静默按「没数据」处理,必须留一条日志让人去人工确认。 if (legacy === 'error') { notify( `警告:读不到旧数据目录 ${legacyDir}(权限不足或路径不可用),本次不做迁移。` + '请人工确认该目录里是否有需要保留的数据' ) return { migrated: false, reason: 'legacy-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, LOCAL_BASE }