69 lines
2.9 KiB
TypeScript
69 lines
2.9 KiB
TypeScript
// MigrationService — 旧版数据迁移(FR-D1/D3)
|
||
import nodePath from 'node:path';
|
||
import { promises as fs } from 'node:fs';
|
||
import { MD_EXT } from '@hunter/shared';
|
||
import { joinFrontmatter } from '../safe/frontmatter-codec.js';
|
||
|
||
export interface MigrationResult {
|
||
migrated: boolean;
|
||
count: number;
|
||
errors: { path: string; reason: string }[];
|
||
}
|
||
|
||
export class MigrationService {
|
||
// FR-D1:检测旧版板块三级结构数据(.hunter/boards/),转换为文件库结构
|
||
async detectAndMigrate(root: string): Promise<MigrationResult> {
|
||
const oldDataDir = nodePath.join(root, '.hunter', 'boards');
|
||
const exists = await fs.access(oldDataDir).then(() => true).catch(() => false);
|
||
if (!exists) return { migrated: false, count: 0, errors: [] };
|
||
|
||
const errors: { path: string; reason: string }[] = [];
|
||
let count = 0;
|
||
try {
|
||
const boards = await fs.readdir(oldDataDir, { withFileTypes: true });
|
||
for (const board of boards) {
|
||
if (!board.isDirectory()) continue;
|
||
const boardDir = nodePath.join(oldDataDir, board.name);
|
||
try {
|
||
const targetDir = nodePath.join(root, board.name);
|
||
await fs.mkdir(targetDir, { recursive: true });
|
||
await this.convertDir(boardDir, targetDir);
|
||
count++;
|
||
} catch (e) {
|
||
errors.push({ path: board.name, reason: e instanceof Error ? e.message : String(e) });
|
||
}
|
||
}
|
||
if (errors.length === 0) {
|
||
await fs.rename(oldDataDir, nodePath.join(root, '.hunter', 'boards.migrated'));
|
||
}
|
||
} catch (e) {
|
||
errors.push({ path: oldDataDir, reason: e instanceof Error ? e.message : String(e) });
|
||
}
|
||
return { migrated: count > 0, count, errors };
|
||
}
|
||
|
||
private async convertDir(srcAbs: string, destAbs: string): Promise<void> {
|
||
const entries = await fs.readdir(srcAbs, { withFileTypes: true });
|
||
for (const e of entries) {
|
||
const srcPath = nodePath.join(srcAbs, e.name);
|
||
if (e.isDirectory()) {
|
||
const destPath = nodePath.join(destAbs, e.name);
|
||
await fs.mkdir(destPath, { recursive: true });
|
||
await this.convertDir(srcPath, destPath);
|
||
} else if (e.isFile() && e.name.endsWith('.json')) {
|
||
const raw = await fs.readFile(srcPath, 'utf8');
|
||
const data = JSON.parse(raw);
|
||
const title = data.title ?? e.name.slice(0, -5);
|
||
const body = data.content ?? '';
|
||
const meta: Record<string, unknown> = { title, createdAt: data.createdAt ?? Date.now() };
|
||
if (data.tags) meta.tags = data.tags;
|
||
if (data.star) meta.star = true;
|
||
const mdName = String(title).replace(/[^\w\u4e00-\u9fa5\-]/g, '_') + MD_EXT;
|
||
await fs.writeFile(nodePath.join(destAbs, mdName), joinFrontmatter(meta as any, body), 'utf8');
|
||
} else if (e.isFile() && e.name.endsWith(MD_EXT)) {
|
||
await fs.copyFile(srcPath, nodePath.join(destAbs, e.name));
|
||
}
|
||
}
|
||
}
|
||
}
|