feat: implement comprehensive item management system with admin UI, API, and asset handling utilities.
All checks were successful
Deploy to Production / test (push) Successful in 44s
All checks were successful
Deploy to Production / test (push) Successful in 44s
This commit is contained in:
239
scripts/migrate-item-assets.ts
Normal file
239
scripts/migrate-item-assets.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user