230 lines
7.0 KiB
JavaScript
230 lines
7.0 KiB
JavaScript
import { connect } from '@tursodatabase/database';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
import { join, resolve } from 'node:path';
|
|
import { randomUUID } from 'node:crypto';
|
|
|
|
const bundleArgument = process.argv
|
|
.slice(2)
|
|
.find((argument) => !argument.startsWith('--'));
|
|
if (!bundleArgument || !process.argv.includes('--confirm-merge')) {
|
|
console.error(
|
|
'Usage: node scripts/managed-images-import.mjs <bundle.json> --confirm-merge [--update-existing]',
|
|
);
|
|
process.exit(2);
|
|
}
|
|
const updateExisting = process.argv.includes('--update-existing');
|
|
const bundlePath = resolve(bundleArgument);
|
|
const bundle = JSON.parse(await readFile(bundlePath, 'utf8'));
|
|
if (
|
|
bundle?.format !== 'shd-build-managed-images' ||
|
|
bundle?.version !== 1 ||
|
|
!Array.isArray(bundle.images)
|
|
) {
|
|
throw new Error(
|
|
'The selected file is not a supported SHD.build managed-image bundle.',
|
|
);
|
|
}
|
|
for (const image of bundle.images) {
|
|
if (
|
|
typeof image?.row?.id !== 'string' ||
|
|
!/^[0-9a-f-]{36}$/i.test(image.row.id) ||
|
|
typeof image.row.storage_key !== 'string' ||
|
|
!/^(?:library|talent|brand_set|os_protocol|skill_chip|specialization|specialization_ability|specialization_node)-[0-9a-f-]{36}\.webp$/i.test(
|
|
image.row.storage_key,
|
|
) ||
|
|
typeof image.contentBase64 !== 'string'
|
|
) {
|
|
throw new Error('The bundle contains an invalid managed image.');
|
|
}
|
|
const isItemRender = ['weapon_render', 'gear_render'].includes(
|
|
image.row.source_entity_type,
|
|
);
|
|
if (
|
|
image.row.source_entity_type &&
|
|
(typeof image.row.source_entity_id !== 'string' ||
|
|
!/^[0-9a-f-]{36}$/i.test(image.row.source_entity_id) ||
|
|
(isItemRender
|
|
? !image.row.storage_key.startsWith('library-')
|
|
: !image.row.storage_key.startsWith(
|
|
`${image.row.source_entity_type}-`,
|
|
)))
|
|
)
|
|
throw new Error('The bundle contains an invalid linked ISAC image.');
|
|
}
|
|
|
|
const backup = spawnSync(process.execPath, ['scripts/db-backup.mjs'], {
|
|
cwd: process.cwd(),
|
|
stdio: 'inherit',
|
|
env: process.env,
|
|
});
|
|
if (backup.error) throw backup.error;
|
|
if (backup.status !== 0)
|
|
throw new Error('Safety backup failed; no managed images were imported.');
|
|
const assetBackup = spawnSync(
|
|
process.execPath,
|
|
['scripts/isac-data-export.mjs'],
|
|
{
|
|
cwd: process.cwd(),
|
|
stdio: 'inherit',
|
|
env: process.env,
|
|
},
|
|
);
|
|
if (assetBackup.error) throw assetBackup.error;
|
|
if (assetBackup.status !== 0)
|
|
throw new Error(
|
|
'Uploaded-asset safety snapshot failed; no managed images were imported.',
|
|
);
|
|
|
|
const databasePath = resolve(
|
|
process.env.TDR_DATABASE_PATH || './data/shd-build-development.db',
|
|
);
|
|
const storageDirectory = resolve(
|
|
process.env.TDR_GAME_ICON_STORAGE_PATH || './data/game-icons',
|
|
);
|
|
const database = await connect(databasePath);
|
|
await mkdir(storageDirectory, { recursive: true });
|
|
let inserted = 0;
|
|
let updated = 0;
|
|
let unchanged = 0;
|
|
|
|
const preflight = [];
|
|
for (const image of bundle.images) {
|
|
const row = image.row;
|
|
const existing = await (
|
|
await database.prepare('SELECT * FROM managed_images WHERE id=?')
|
|
).get(row.id);
|
|
if (existing && !updateExisting) {
|
|
const same = [
|
|
'name',
|
|
'category',
|
|
'tags_json',
|
|
'is_icon',
|
|
'force_white',
|
|
'storage_key',
|
|
'byte_size',
|
|
'width',
|
|
'height',
|
|
'source_entity_type',
|
|
'source_entity_id',
|
|
].every((key) => String(existing[key]) === String(row[key]));
|
|
if (!same)
|
|
throw new Error(
|
|
`Image ${row.id} already exists with different metadata. Re-run with --update-existing after review.`,
|
|
);
|
|
unchanged++;
|
|
continue;
|
|
}
|
|
const creator = await (
|
|
await database.prepare('SELECT id FROM users WHERE id=?')
|
|
).get(row.created_by);
|
|
if (!creator)
|
|
throw new Error(
|
|
`Image "${row.name}" references a creator that does not exist in the target database.`,
|
|
);
|
|
const nameConflict = await (
|
|
await database.prepare(
|
|
'SELECT id FROM managed_images WHERE name=? COLLATE NOCASE AND id<>?',
|
|
)
|
|
).get(row.name, row.id);
|
|
if (nameConflict)
|
|
throw new Error(
|
|
`Image name "${row.name}" is already assigned to a different target asset.`,
|
|
);
|
|
if (row.source_entity_type && row.source_entity_id) {
|
|
const sourceConflict = await (
|
|
await database.prepare(
|
|
'SELECT id FROM managed_images WHERE source_entity_type=? AND source_entity_id=? AND id<>?',
|
|
)
|
|
).get(row.source_entity_type, row.source_entity_id, row.id);
|
|
if (sourceConflict)
|
|
throw new Error(
|
|
`The ${row.source_entity_type} image ${row.source_entity_id} is already registered under a different library asset.`,
|
|
);
|
|
}
|
|
preflight.push({ image, existing });
|
|
}
|
|
|
|
await database.exec('BEGIN');
|
|
try {
|
|
for (const { image, existing } of preflight) {
|
|
const row = image.row;
|
|
await writeFile(
|
|
join(storageDirectory, row.storage_key),
|
|
Buffer.from(image.contentBase64, 'base64'),
|
|
{
|
|
mode: 0o600,
|
|
flag: existing || row.source_entity_type ? 'w' : 'wx',
|
|
},
|
|
);
|
|
if (existing) {
|
|
await (
|
|
await database.prepare(`UPDATE managed_images SET
|
|
name=?,category=?,tags_json=?,is_icon=?,force_white=?,storage_key=?,original_filename=?,
|
|
byte_size=?,width=?,height=?,source_entity_type=?,source_entity_id=?,updated_at=? WHERE id=?`)
|
|
).run(
|
|
row.name,
|
|
row.category,
|
|
row.tags_json,
|
|
row.is_icon,
|
|
row.force_white,
|
|
row.storage_key,
|
|
row.original_filename,
|
|
row.byte_size,
|
|
row.width,
|
|
row.height,
|
|
row.source_entity_type ?? null,
|
|
row.source_entity_id ?? null,
|
|
row.updated_at,
|
|
row.id,
|
|
);
|
|
updated++;
|
|
} else {
|
|
await (
|
|
await database.prepare(`INSERT INTO managed_images
|
|
(id,name,category,tags_json,is_icon,force_white,storage_key,original_filename,byte_size,width,height,
|
|
source_entity_type,source_entity_id,created_by,created_at,updated_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
|
|
).run(
|
|
row.id,
|
|
row.name,
|
|
row.category,
|
|
row.tags_json,
|
|
row.is_icon,
|
|
row.force_white,
|
|
row.storage_key,
|
|
row.original_filename,
|
|
row.byte_size,
|
|
row.width,
|
|
row.height,
|
|
row.source_entity_type ?? null,
|
|
row.source_entity_id ?? null,
|
|
row.created_by,
|
|
row.created_at,
|
|
row.updated_at,
|
|
);
|
|
inserted++;
|
|
}
|
|
await (
|
|
await database.prepare(`INSERT INTO audit_log
|
|
(id,actor_id,entity_type,entity_id,action,before_json,after_json,created_at) VALUES(?,?,?,?,?,?,?,?)`)
|
|
).run(
|
|
randomUUID(),
|
|
row.created_by,
|
|
'system_configuration',
|
|
row.id,
|
|
existing ? 'image.import_updated' : 'image.imported',
|
|
existing ? JSON.stringify(existing) : null,
|
|
JSON.stringify(row),
|
|
new Date().toISOString(),
|
|
);
|
|
}
|
|
await database.exec('COMMIT');
|
|
} catch (error) {
|
|
await database.exec('ROLLBACK');
|
|
throw error;
|
|
}
|
|
console.log(
|
|
`Managed-image merge complete: ${inserted} inserted, ${updated} updated, ${unchanged} unchanged.`,
|
|
);
|