'use strict'; /** * 老数据目录(%USERPROFILE%\.clipboard-app)→ 新默认目录(项目/exe 同级 data/) * 的一次性迁移。 * * 三条底线: * 1. 迁完 config.json 里的 dataDir 必须指向新目录 —— 否则下次启动又跑回老地方; * 2. 新目录已经在用时不得覆盖(幂等,重复启动不会拿老库盖新库); * 3. 老文件保留 —— 复制不是移动,出问题能回退。 */ const assert = require('node:assert'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const root = fs.mkdtempSync(path.join(os.tmpdir(), 'clip-mig-')); const home = path.join(root, 'home'); const target = path.join(root, 'Clipboard Data'); // 模拟 D:\Clipboard Data const legacy = path.join(home, '.clipboard-app'); process.env.USERPROFILE = home; process.env.HOME = home; process.env.CLIPBOARD_APP_DATA_DIR = target; const cfg = require('../config.js'); let passed = 0; function check(name, fn) { try { fn(); console.log(' ok -', name); passed++; } catch (e) { console.error(' FAIL -', name, '\n', e.message); process.exitCode = 1; } } function resetTarget() { fs.rmSync(target, { recursive: true, force: true }); } function seedLegacy(extraCfg) { fs.rmSync(legacy, { recursive: true, force: true }); fs.mkdirSync(legacy, { recursive: true }); fs.writeFileSync(path.join(legacy, 'history.db'), 'OLD-HISTORY'); fs.writeFileSync(path.join(legacy, 'history.db-wal'), 'OLD-WAL'); fs.writeFileSync(path.join(legacy, 'favorites.db'), 'OLD-FAV'); fs.writeFileSync(path.join(legacy, 'history.db-shm'), 'OLD-SHM'); fs.writeFileSync( path.join(legacy, 'config.json'), JSON.stringify({ maxItems: 500, pollMs: 500, theme: 'carbon', dataDir: legacy, ...extraCfg }), ); } check('老数据被复制到新目录,且 dataDir 改写为新目录', () => { resetTarget(); seedLegacy(); const r = cfg.migrateLegacyData(); assert.strictEqual(r.migrated, true); assert.strictEqual(fs.readFileSync(path.join(target, 'history.db'), 'utf8'), 'OLD-HISTORY'); assert.strictEqual(fs.readFileSync(path.join(target, 'history.db-wal'), 'utf8'), 'OLD-WAL'); assert.strictEqual(fs.readFileSync(path.join(target, 'favorites.db'), 'utf8'), 'OLD-FAV'); const c = cfg.loadConfig(); assert.strictEqual(path.resolve(c.dataDir), path.resolve(target), 'dataDir 必须指向新目录'); assert.strictEqual(c.maxItems, 500, '其他设置要保留'); assert.strictEqual(c.theme, 'carbon'); }); check('-shm 不搬(SQLite 自己重建)', () => { assert.ok(!fs.existsSync(path.join(target, 'history.db-shm'))); }); check('老文件保留,不是移动', () => { assert.ok(fs.existsSync(path.join(legacy, 'history.db'))); assert.ok(fs.existsSync(path.join(legacy, 'config.json'))); }); check('幂等:新目录已在用时不覆盖', () => { fs.writeFileSync(path.join(target, 'history.db'), 'NEW-HISTORY'); const r = cfg.migrateLegacyData(); assert.strictEqual(r.migrated, false); assert.strictEqual(r.reason, 'target-in-use'); assert.strictEqual(fs.readFileSync(path.join(target, 'history.db'), 'utf8'), 'NEW-HISTORY'); }); check('没有老数据时什么都不做', () => { resetTarget(); fs.rmSync(legacy, { recursive: true, force: true }); const r = cfg.migrateLegacyData(); assert.strictEqual(r.migrated, false); assert.strictEqual(r.reason, 'nothing-to-migrate'); assert.ok(!fs.existsSync(path.join(target, 'config.json'))); }); check('老 config 指向自定义目录时,从那个目录搬', () => { resetTarget(); const custom = path.join(root, 'custom'); fs.mkdirSync(custom, { recursive: true }); fs.writeFileSync(path.join(custom, 'history.db'), 'CUSTOM-HISTORY'); seedLegacy({ dataDir: custom }); const r = cfg.migrateLegacyData(); assert.strictEqual(r.migrated, true); assert.strictEqual(path.resolve(r.from), path.resolve(custom)); assert.strictEqual(fs.readFileSync(path.join(target, 'history.db'), 'utf8'), 'CUSTOM-HISTORY'); assert.strictEqual(path.resolve(cfg.loadConfig().dataDir), path.resolve(target)); }); check('老 config 缺 dataDir 字段时,从老目录搬(不能用 sanitize 回填的新默认目录当源)', () => { // sanitize 会把缺省的 dataDir 补成新默认目录(= target),拿它当 src // 就是"从 target 搬到 target":什么也没搬却报 migrated,老数据永久遗留。 resetTarget(); fs.rmSync(legacy, { recursive: true, force: true }); fs.mkdirSync(legacy, { recursive: true }); fs.writeFileSync(path.join(legacy, 'history.db'), 'OLD-HISTORY'); fs.writeFileSync(path.join(legacy, 'config.json'), JSON.stringify({ maxItems: 100, pollMs: 500 })); const r = cfg.migrateLegacyData(); assert.strictEqual(r.migrated, true); assert.strictEqual(path.resolve(r.from), path.resolve(legacy), 'src 必须是老目录'); assert.strictEqual(fs.readFileSync(path.join(target, 'history.db'), 'utf8'), 'OLD-HISTORY'); }); fs.rmSync(root, { recursive: true, force: true }); console.log(`\n${passed} checks passed`);