80 lines
3.6 KiB
JavaScript
80 lines
3.6 KiB
JavaScript
import { connect } from '@tursodatabase/database';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
import { basename, join, resolve } from 'node:path';
|
|
import process from 'node:process';
|
|
import { ISAC_DATA_TABLES, quoteIdentifier } from './isac-data-tables.mjs';
|
|
|
|
const snapshotArgument = process.argv.slice(2).find((argument) => !argument.startsWith('--'));
|
|
if (!snapshotArgument || !process.argv.includes('--confirm-replace-isac-data')) {
|
|
console.error('Usage: node scripts/isac-data-import.mjs <snapshot.json> --confirm-replace-isac-data');
|
|
process.exit(2);
|
|
}
|
|
|
|
const snapshotPath = resolve(snapshotArgument);
|
|
const snapshot = JSON.parse(await readFile(snapshotPath, 'utf8'));
|
|
if (snapshot?.format !== 'shd-build-isac-data' || snapshot?.version !== 1 || !Array.isArray(snapshot.tables) || !Array.isArray(snapshot.icons)) {
|
|
throw new Error('The selected file is not a supported SHD.build ISAC data snapshot.');
|
|
}
|
|
const receivedNames = snapshot.tables.map((table) => table.name);
|
|
if (receivedNames.length !== ISAC_DATA_TABLES.length || ISAC_DATA_TABLES.some((name, index) => receivedNames[index] !== name)) {
|
|
throw new Error('The ISAC snapshot table manifest is incomplete or out of order.');
|
|
}
|
|
for (const icon of snapshot.icons) {
|
|
if (typeof icon.storageKey !== 'string' || !/^[a-z_]+-[0-9a-f-]{36}\.webp$/i.test(icon.storageKey)
|
|
|| typeof icon.contentBase64 !== 'string') {
|
|
throw new Error('The ISAC snapshot contains an invalid uploaded icon.');
|
|
}
|
|
}
|
|
|
|
const timestamp = new Date().toISOString().replaceAll(':', '-').replace(/\.\d{3}Z$/, 'Z');
|
|
const databasePath = resolve(process.env.TDR_DATABASE_PATH || './data/shd-build-development.db');
|
|
const backupPath = resolve(`./data/backups/${basename(databasePath).replace(/\.[^.]+$/, '')}-before-isac-import-${timestamp}.json`);
|
|
const isacBackupPath = resolve(`./data/backups/isac-data-before-import-${timestamp}.json`);
|
|
for (const [script, output] of [
|
|
['scripts/db-backup.mjs', backupPath],
|
|
['scripts/isac-data-export.mjs', isacBackupPath]
|
|
]) {
|
|
const result = spawnSync(process.execPath, [script, output], { cwd: process.cwd(), stdio: 'inherit', env: process.env });
|
|
if (result.error) throw result.error;
|
|
if (result.status !== 0) throw new Error(`Safety backup failed in ${script}; no ISAC data was changed.`);
|
|
}
|
|
|
|
const database = await connect(databasePath);
|
|
let importedRows = 0;
|
|
await database.exec('PRAGMA foreign_keys=OFF');
|
|
await database.exec('BEGIN');
|
|
try {
|
|
for (const name of [...ISAC_DATA_TABLES].reverse()) {
|
|
await database.exec(`DELETE FROM ${quoteIdentifier(name)}`);
|
|
}
|
|
for (const table of snapshot.tables) {
|
|
for (const row of table.rows) {
|
|
const columns = Object.keys(row);
|
|
if (!columns.length) continue;
|
|
const insert = await database.prepare(
|
|
`INSERT INTO ${quoteIdentifier(table.name)} (${columns.map(quoteIdentifier).join(', ')})
|
|
VALUES (${columns.map(() => '?').join(', ')})`
|
|
);
|
|
await insert.run(...columns.map((column) => row[column]));
|
|
importedRows++;
|
|
}
|
|
}
|
|
await database.exec('COMMIT');
|
|
} catch (error) {
|
|
await database.exec('ROLLBACK');
|
|
throw error;
|
|
} finally {
|
|
await database.exec('PRAGMA foreign_keys=ON');
|
|
}
|
|
|
|
const iconDirectory = resolve(process.env.TDR_GAME_ICON_STORAGE_PATH || './data/game-icons');
|
|
await mkdir(iconDirectory, { recursive: true });
|
|
for (const icon of snapshot.icons) {
|
|
await writeFile(join(iconDirectory, icon.storageKey), Buffer.from(icon.contentBase64, 'base64'), { mode: 0o600 });
|
|
}
|
|
|
|
console.log(`Imported ${importedRows} ISAC rows and ${snapshot.icons.length} uploaded icons from ${snapshotPath}.`);
|
|
console.log(`Full pre-import backup: ${backupPath}`);
|
|
console.log(`ISAC pre-import snapshot: ${isacBackupPath}`);
|