Files
aurorabot/scripts/migrate-item-assets.ts
syntaxbullet 34958aa220
All checks were successful
Deploy to Production / test (push) Successful in 44s
feat: implement comprehensive item management system with admin UI, API, and asset handling utilities.
2026-02-06 12:19:14 +01:00

240 lines
7.6 KiB
TypeScript

#!/usr/bin/env bun
/**
* Item Asset Migration Script
*
* Downloads images from existing Discord CDN URLs and saves them locally.
* Updates database records to use local asset paths.
*
* Usage:
* bun run scripts/migrate-item-assets.ts # Dry run (no changes)
* bun run scripts/migrate-item-assets.ts --execute # Actually perform migration
*/
import { resolve, join } from "path";
import { mkdir } from "node:fs/promises";
// Initialize database connection
const { DrizzleClient } = await import("../shared/db/DrizzleClient");
const { items } = await import("../shared/db/schema");
const ASSETS_DIR = resolve(import.meta.dir, "../bot/assets/graphics/items");
const DRY_RUN = !process.argv.includes("--execute");
interface MigrationResult {
itemId: number;
itemName: string;
originalUrl: string;
newPath: string;
status: "success" | "skipped" | "failed";
error?: string;
}
/**
* Check if a URL is an external URL (not a local asset path)
*/
function isExternalUrl(url: string | null): boolean {
if (!url) return false;
return url.startsWith("http://") || url.startsWith("https://");
}
/**
* Check if a URL is likely a Discord CDN URL
*/
function isDiscordCdnUrl(url: string): boolean {
return url.includes("cdn.discordapp.com") ||
url.includes("media.discordapp.net") ||
url.includes("discord.gg");
}
/**
* Download an image from a URL and save it locally
*/
async function downloadImage(url: string, destPath: string): Promise<void> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get("content-type") || "";
if (!contentType.startsWith("image/")) {
throw new Error(`Invalid content type: ${contentType}`);
}
const buffer = await response.arrayBuffer();
await Bun.write(destPath, buffer);
}
/**
* Migrate a single item's images
*/
async function migrateItem(item: {
id: number;
name: string;
iconUrl: string | null;
imageUrl: string | null;
}): Promise<MigrationResult> {
const result: MigrationResult = {
itemId: item.id,
itemName: item.name,
originalUrl: item.iconUrl || item.imageUrl || "",
newPath: `/assets/items/${item.id}.png`,
status: "skipped"
};
// Check if either URL needs migration
const hasExternalIcon = isExternalUrl(item.iconUrl);
const hasExternalImage = isExternalUrl(item.imageUrl);
if (!hasExternalIcon && !hasExternalImage) {
result.status = "skipped";
return result;
}
// Prefer iconUrl, fall back to imageUrl
const urlToDownload = item.iconUrl || item.imageUrl;
if (!urlToDownload || !isExternalUrl(urlToDownload)) {
result.status = "skipped";
return result;
}
result.originalUrl = urlToDownload;
const destPath = join(ASSETS_DIR, `${item.id}.png`);
if (DRY_RUN) {
console.log(` [DRY RUN] Would download: ${urlToDownload}`);
console.log(` -> ${destPath}`);
result.status = "success";
return result;
}
try {
// Download the image
await downloadImage(urlToDownload, destPath);
// Update database record
const { eq } = await import("drizzle-orm");
await DrizzleClient
.update(items)
.set({
iconUrl: `/assets/items/${item.id}.png`,
imageUrl: `/assets/items/${item.id}.png`,
})
.where(eq(items.id, item.id));
result.status = "success";
console.log(` ✅ Migrated: ${item.name} (ID: ${item.id})`);
} catch (error) {
result.status = "failed";
result.error = error instanceof Error ? error.message : String(error);
console.log(` ❌ Failed: ${item.name} (ID: ${item.id}) - ${result.error}`);
}
return result;
}
/**
* Main migration function
*/
async function main() {
console.log("═══════════════════════════════════════════════════════════════");
console.log(" Item Asset Migration Script");
console.log("═══════════════════════════════════════════════════════════════");
console.log();
if (DRY_RUN) {
console.log(" ⚠️ DRY RUN MODE - No changes will be made");
console.log(" Run with --execute to perform actual migration");
console.log();
}
// Ensure assets directory exists
await mkdir(ASSETS_DIR, { recursive: true });
console.log(` 📁 Assets directory: ${ASSETS_DIR}`);
console.log();
// Fetch all items
const allItems = await DrizzleClient.select({
id: items.id,
name: items.name,
iconUrl: items.iconUrl,
imageUrl: items.imageUrl,
}).from(items);
console.log(` 📦 Found ${allItems.length} total items`);
// Filter items that need migration
const itemsToMigrate = allItems.filter(item =>
isExternalUrl(item.iconUrl) || isExternalUrl(item.imageUrl)
);
console.log(` 🔄 ${itemsToMigrate.length} items have external URLs`);
console.log();
if (itemsToMigrate.length === 0) {
console.log(" ✨ No items need migration!");
return;
}
// Categorize by URL type
const discordCdnItems = itemsToMigrate.filter(item =>
isDiscordCdnUrl(item.iconUrl || "") || isDiscordCdnUrl(item.imageUrl || "")
);
const otherExternalItems = itemsToMigrate.filter(item =>
!isDiscordCdnUrl(item.iconUrl || "") && !isDiscordCdnUrl(item.imageUrl || "")
);
console.log(` 📊 Breakdown:`);
console.log(` - Discord CDN URLs: ${discordCdnItems.length}`);
console.log(` - Other external URLs: ${otherExternalItems.length}`);
console.log();
// Process migrations
console.log(" Starting migration...");
console.log();
const results: MigrationResult[] = [];
for (const item of itemsToMigrate) {
const result = await migrateItem(item);
results.push(result);
}
// Summary
console.log();
console.log("═══════════════════════════════════════════════════════════════");
console.log(" Migration Summary");
console.log("═══════════════════════════════════════════════════════════════");
const successful = results.filter(r => r.status === "success").length;
const skipped = results.filter(r => r.status === "skipped").length;
const failed = results.filter(r => r.status === "failed").length;
console.log(` ✅ Successful: ${successful}`);
console.log(` ⏭️ Skipped: ${skipped}`);
console.log(` ❌ Failed: ${failed}`);
console.log();
if (failed > 0) {
console.log(" Failed items:");
for (const result of results.filter(r => r.status === "failed")) {
console.log(` - ${result.itemName}: ${result.error}`);
}
}
if (DRY_RUN) {
console.log();
console.log(" ⚠️ This was a dry run. Run with --execute to apply changes.");
}
// Exit with error code if any failures
process.exit(failed > 0 ? 1 : 0);
}
// Run
main().catch(error => {
console.error("Migration failed:", error);
process.exit(1);
});