import { spawnSync } from 'node:child_process'; import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'; import { dirname, join, resolve } from 'node:path'; import process from 'node:process'; const outputArgument = process.argv[2]; const timestamp = new Date() .toISOString() .replaceAll(':', '-') .replace(/\.\d{3}Z$/, 'Z'); const outputPath = resolve( outputArgument || `./data/backups/shd-stage-bootstrap-${timestamp}.json`, ); const temporaryBackupPath = `${outputPath}.database.json`; const databaseBackup = spawnSync( process.execPath, ['scripts/db-backup.mjs', temporaryBackupPath], { cwd: process.cwd(), stdio: 'inherit', env: process.env, }, ); if (databaseBackup.error) throw databaseBackup.error; if (databaseBackup.status !== 0) throw new Error( 'Database backup failed; no staging bootstrap bundle was created.', ); async function collectFiles(directory, pattern) { try { const names = (await readdir(directory)) .filter((name) => pattern.test(name)) .sort(); return await Promise.all( names.map(async (name) => ({ name, contentBase64: (await readFile(join(directory, name))).toString( 'base64', ), })), ); } catch (error) { if (error?.code === 'ENOENT') return []; throw error; } } try { const database = JSON.parse(await readFile(temporaryBackupPath, 'utf8')); const avatars = await collectFiles( resolve(process.env.TDR_AVATAR_STORAGE_PATH || './data/avatars'), /^[0-9a-f-]{36}\.webp$/i, ); const gameIcons = await collectFiles( resolve(process.env.TDR_GAME_ICON_STORAGE_PATH || './data/game-icons'), /^[a-z_]+-[0-9a-f-]{36}\.webp$/i, ); const bundle = { format: 'shd-build-stage-bootstrap', version: 1, createdAt: new Date().toISOString(), database, files: { avatars, gameIcons }, }; await mkdir(dirname(outputPath), { recursive: true }); await writeFile(outputPath, `${JSON.stringify(bundle, null, 2)}\n`, { encoding: 'utf8', flag: 'wx', }); console.log( `Staging bootstrap bundle includes ${avatars.length} avatars and ${gameIcons.length} game-data icons.`, ); console.log(`Bootstrap bundle: ${outputPath}`); } finally { await rm(temporaryBackupPath, { force: true }); }