| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- /**
- * Migration script: Move uploads from public/uploads/ to data/uploads/[orgId]/
- * and update all database references.
- *
- * This script:
- * 1. For each organization, creates data/uploads/[orgId]/vehicles|inventory|services|logos/
- * 2. Copies files from public/uploads/[category]/ to the correct org directory
- * 3. Updates imageUrl/fileUrl in the database from /uploads/[category]/file
- * to /api/files/[orgId]/[category]/file
- * 4. Updates AppSetting logo URLs
- *
- * Usage:
- * npx tsx scripts/migrate-uploads.ts
- *
- * Run this AFTER deploying the new code. The resolveUploadPath helper supports
- * both old and new URL formats, so the app works during the transition.
- */
- import { PrismaClient } from "@prisma/client";
- import { copyFile, mkdir, stat } from "fs/promises";
- import path from "path";
- const db = new PrismaClient();
- const PUBLIC_UPLOADS = path.join(process.cwd(), "public", "uploads");
- const DATA_UPLOADS = path.join(process.cwd(), "data", "uploads");
- function parseOldUrl(url: string): { category: string; filename: string } | null {
- // Match /uploads/category/filename
- const match = url.match(/^\/uploads\/([^/]+)\/([^/]+)$/);
- if (!match) return null;
- return { category: match[1], filename: match[2] };
- }
- async function fileExists(filePath: string): Promise<boolean> {
- try {
- await stat(filePath);
- return true;
- } catch {
- return false;
- }
- }
- async function main() {
- console.log("Starting upload migration...\n");
- // Get all organizations
- const orgs = await db.organization.findMany({ select: { id: true, name: true } });
- console.log(`Found ${orgs.length} organizations.\n`);
- // Create directories for each org
- for (const org of orgs) {
- for (const category of ["vehicles", "inventory", "services", "logos"]) {
- await mkdir(path.join(DATA_UPLOADS, org.id, category), { recursive: true });
- }
- }
- let totalMigrated = 0;
- let totalSkipped = 0;
- // 1. Migrate Vehicle images
- console.log("--- Migrating vehicle images ---");
- const vehicles = await db.vehicle.findMany({
- where: { imageUrl: { not: null } },
- select: { id: true, imageUrl: true, organizationId: true },
- });
- for (const v of vehicles) {
- if (!v.imageUrl || v.imageUrl.startsWith("/api/files/") || !v.organizationId) continue;
- const parsed = parseOldUrl(v.imageUrl);
- if (!parsed) {
- console.log(` [skip] Vehicle ${v.id}: unrecognized URL "${v.imageUrl}"`);
- totalSkipped++;
- continue;
- }
- const orgId = v.organizationId;
- const src = path.join(PUBLIC_UPLOADS, parsed.category, parsed.filename);
- const dst = path.join(DATA_UPLOADS, orgId, "vehicles", parsed.filename);
- const newUrl = `/api/files/${orgId}/vehicles/${parsed.filename}`;
- if (await fileExists(src)) {
- await copyFile(src, dst);
- await db.vehicle.update({ where: { id: v.id }, data: { imageUrl: newUrl } });
- console.log(` [ok] Vehicle ${v.id}: ${v.imageUrl} -> ${newUrl}`);
- totalMigrated++;
- } else {
- console.log(` [missing] Vehicle ${v.id}: source file not found "${src}"`);
- // Update URL anyway so it points to the new path format
- await db.vehicle.update({ where: { id: v.id }, data: { imageUrl: newUrl } });
- totalSkipped++;
- }
- }
- // 2. Migrate ServiceAttachment files
- console.log("\n--- Migrating service attachments ---");
- const attachments = await db.serviceAttachment.findMany({
- select: {
- id: true,
- fileUrl: true,
- serviceRecord: {
- select: { vehicle: { select: { organizationId: true } } },
- },
- },
- });
- for (const att of attachments) {
- if (att.fileUrl.startsWith("/api/files/")) continue;
- const parsed = parseOldUrl(att.fileUrl);
- if (!parsed) {
- console.log(` [skip] Attachment ${att.id}: unrecognized URL "${att.fileUrl}"`);
- totalSkipped++;
- continue;
- }
- const orgId = att.serviceRecord.vehicle.organizationId;
- if (!orgId) continue;
- const src = path.join(PUBLIC_UPLOADS, parsed.category, parsed.filename);
- const dst = path.join(DATA_UPLOADS, orgId, "services", parsed.filename);
- const newUrl = `/api/files/${orgId}/services/${parsed.filename}`;
- if (await fileExists(src)) {
- await copyFile(src, dst);
- await db.serviceAttachment.update({ where: { id: att.id }, data: { fileUrl: newUrl } });
- console.log(` [ok] Attachment ${att.id}: ${att.fileUrl} -> ${newUrl}`);
- totalMigrated++;
- } else {
- console.log(` [missing] Attachment ${att.id}: source file not found "${src}"`);
- await db.serviceAttachment.update({ where: { id: att.id }, data: { fileUrl: newUrl } });
- totalSkipped++;
- }
- }
- // 3. Migrate InventoryPart images
- console.log("\n--- Migrating inventory images ---");
- const parts = await db.inventoryPart.findMany({
- where: { imageUrl: { not: null } },
- select: { id: true, imageUrl: true, organizationId: true },
- });
- for (const p of parts) {
- if (!p.imageUrl || p.imageUrl.startsWith("/api/files/") || !p.organizationId) continue;
- const parsed = parseOldUrl(p.imageUrl);
- if (!parsed) {
- console.log(` [skip] Part ${p.id}: unrecognized URL "${p.imageUrl}"`);
- totalSkipped++;
- continue;
- }
- const orgId = p.organizationId;
- const src = path.join(PUBLIC_UPLOADS, parsed.category, parsed.filename);
- const dst = path.join(DATA_UPLOADS, orgId, "inventory", parsed.filename);
- const newUrl = `/api/files/${orgId}/inventory/${parsed.filename}`;
- if (await fileExists(src)) {
- await copyFile(src, dst);
- await db.inventoryPart.update({ where: { id: p.id }, data: { imageUrl: newUrl } });
- console.log(` [ok] Part ${p.id}: ${p.imageUrl} -> ${newUrl}`);
- totalMigrated++;
- } else {
- console.log(` [missing] Part ${p.id}: source file not found "${src}"`);
- await db.inventoryPart.update({ where: { id: p.id }, data: { imageUrl: newUrl } });
- totalSkipped++;
- }
- }
- // 4. Migrate AppSetting logos
- console.log("\n--- Migrating logo settings ---");
- const logos = await db.appSetting.findMany({
- where: { key: "workshop.logo" },
- select: { id: true, value: true, organizationId: true },
- });
- for (const logo of logos) {
- if (logo.value.startsWith("/api/files/") || !logo.organizationId) continue;
- const parsed = parseOldUrl(logo.value);
- if (!parsed) {
- console.log(` [skip] Logo setting ${logo.id}: unrecognized URL "${logo.value}"`);
- totalSkipped++;
- continue;
- }
- const orgId = logo.organizationId;
- const src = path.join(PUBLIC_UPLOADS, parsed.category, parsed.filename);
- const dst = path.join(DATA_UPLOADS, orgId, "logos", parsed.filename);
- const newUrl = `/api/files/${orgId}/logos/${parsed.filename}`;
- if (await fileExists(src)) {
- await copyFile(src, dst);
- await db.appSetting.update({ where: { id: logo.id }, data: { value: newUrl } });
- console.log(` [ok] Logo ${logo.id}: ${logo.value} -> ${newUrl}`);
- totalMigrated++;
- } else {
- console.log(` [missing] Logo ${logo.id}: source file not found "${src}"`);
- await db.appSetting.update({ where: { id: logo.id }, data: { value: newUrl } });
- totalSkipped++;
- }
- }
- console.log(`\nMigration complete! Migrated: ${totalMigrated}, Skipped: ${totalSkipped}`);
- console.log("\nYou can now safely remove public/uploads/ after verifying everything works.");
- }
- main()
- .catch((e) => {
- console.error("Migration failed:", e);
- process.exit(1);
- })
- .finally(() => db.$disconnect());
|