4
0

migrate-uploads.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. /**
  2. * Migration script: Move uploads from public/uploads/ to data/uploads/[orgId]/
  3. * and update all database references.
  4. *
  5. * This script:
  6. * 1. For each organization, creates data/uploads/[orgId]/vehicles|inventory|services|logos/
  7. * 2. Copies files from public/uploads/[category]/ to the correct org directory
  8. * 3. Updates imageUrl/fileUrl in the database from /uploads/[category]/file
  9. * to /api/files/[orgId]/[category]/file
  10. * 4. Updates AppSetting logo URLs
  11. *
  12. * Usage:
  13. * npx tsx scripts/migrate-uploads.ts
  14. *
  15. * Run this AFTER deploying the new code. The resolveUploadPath helper supports
  16. * both old and new URL formats, so the app works during the transition.
  17. */
  18. import { PrismaClient } from "@prisma/client";
  19. import { copyFile, mkdir, stat } from "fs/promises";
  20. import path from "path";
  21. const db = new PrismaClient();
  22. const PUBLIC_UPLOADS = path.join(process.cwd(), "public", "uploads");
  23. const DATA_UPLOADS = path.join(process.cwd(), "data", "uploads");
  24. function parseOldUrl(url: string): { category: string; filename: string } | null {
  25. // Match /uploads/category/filename
  26. const match = url.match(/^\/uploads\/([^/]+)\/([^/]+)$/);
  27. if (!match) return null;
  28. return { category: match[1], filename: match[2] };
  29. }
  30. async function fileExists(filePath: string): Promise<boolean> {
  31. try {
  32. await stat(filePath);
  33. return true;
  34. } catch {
  35. return false;
  36. }
  37. }
  38. async function main() {
  39. console.log("Starting upload migration...\n");
  40. // Get all organizations
  41. const orgs = await db.organization.findMany({ select: { id: true, name: true } });
  42. console.log(`Found ${orgs.length} organizations.\n`);
  43. // Create directories for each org
  44. for (const org of orgs) {
  45. for (const category of ["vehicles", "inventory", "services", "logos"]) {
  46. await mkdir(path.join(DATA_UPLOADS, org.id, category), { recursive: true });
  47. }
  48. }
  49. let totalMigrated = 0;
  50. let totalSkipped = 0;
  51. // 1. Migrate Vehicle images
  52. console.log("--- Migrating vehicle images ---");
  53. const vehicles = await db.vehicle.findMany({
  54. where: { imageUrl: { not: null } },
  55. select: { id: true, imageUrl: true, organizationId: true },
  56. });
  57. for (const v of vehicles) {
  58. if (!v.imageUrl || v.imageUrl.startsWith("/api/files/") || !v.organizationId) continue;
  59. const parsed = parseOldUrl(v.imageUrl);
  60. if (!parsed) {
  61. console.log(` [skip] Vehicle ${v.id}: unrecognized URL "${v.imageUrl}"`);
  62. totalSkipped++;
  63. continue;
  64. }
  65. const orgId = v.organizationId;
  66. const src = path.join(PUBLIC_UPLOADS, parsed.category, parsed.filename);
  67. const dst = path.join(DATA_UPLOADS, orgId, "vehicles", parsed.filename);
  68. const newUrl = `/api/files/${orgId}/vehicles/${parsed.filename}`;
  69. if (await fileExists(src)) {
  70. await copyFile(src, dst);
  71. await db.vehicle.update({ where: { id: v.id }, data: { imageUrl: newUrl } });
  72. console.log(` [ok] Vehicle ${v.id}: ${v.imageUrl} -> ${newUrl}`);
  73. totalMigrated++;
  74. } else {
  75. console.log(` [missing] Vehicle ${v.id}: source file not found "${src}"`);
  76. // Update URL anyway so it points to the new path format
  77. await db.vehicle.update({ where: { id: v.id }, data: { imageUrl: newUrl } });
  78. totalSkipped++;
  79. }
  80. }
  81. // 2. Migrate ServiceAttachment files
  82. console.log("\n--- Migrating service attachments ---");
  83. const attachments = await db.serviceAttachment.findMany({
  84. select: {
  85. id: true,
  86. fileUrl: true,
  87. serviceRecord: {
  88. select: { vehicle: { select: { organizationId: true } } },
  89. },
  90. },
  91. });
  92. for (const att of attachments) {
  93. if (att.fileUrl.startsWith("/api/files/")) continue;
  94. const parsed = parseOldUrl(att.fileUrl);
  95. if (!parsed) {
  96. console.log(` [skip] Attachment ${att.id}: unrecognized URL "${att.fileUrl}"`);
  97. totalSkipped++;
  98. continue;
  99. }
  100. const orgId = att.serviceRecord.vehicle.organizationId;
  101. if (!orgId) continue;
  102. const src = path.join(PUBLIC_UPLOADS, parsed.category, parsed.filename);
  103. const dst = path.join(DATA_UPLOADS, orgId, "services", parsed.filename);
  104. const newUrl = `/api/files/${orgId}/services/${parsed.filename}`;
  105. if (await fileExists(src)) {
  106. await copyFile(src, dst);
  107. await db.serviceAttachment.update({ where: { id: att.id }, data: { fileUrl: newUrl } });
  108. console.log(` [ok] Attachment ${att.id}: ${att.fileUrl} -> ${newUrl}`);
  109. totalMigrated++;
  110. } else {
  111. console.log(` [missing] Attachment ${att.id}: source file not found "${src}"`);
  112. await db.serviceAttachment.update({ where: { id: att.id }, data: { fileUrl: newUrl } });
  113. totalSkipped++;
  114. }
  115. }
  116. // 3. Migrate InventoryPart images
  117. console.log("\n--- Migrating inventory images ---");
  118. const parts = await db.inventoryPart.findMany({
  119. where: { imageUrl: { not: null } },
  120. select: { id: true, imageUrl: true, organizationId: true },
  121. });
  122. for (const p of parts) {
  123. if (!p.imageUrl || p.imageUrl.startsWith("/api/files/") || !p.organizationId) continue;
  124. const parsed = parseOldUrl(p.imageUrl);
  125. if (!parsed) {
  126. console.log(` [skip] Part ${p.id}: unrecognized URL "${p.imageUrl}"`);
  127. totalSkipped++;
  128. continue;
  129. }
  130. const orgId = p.organizationId;
  131. const src = path.join(PUBLIC_UPLOADS, parsed.category, parsed.filename);
  132. const dst = path.join(DATA_UPLOADS, orgId, "inventory", parsed.filename);
  133. const newUrl = `/api/files/${orgId}/inventory/${parsed.filename}`;
  134. if (await fileExists(src)) {
  135. await copyFile(src, dst);
  136. await db.inventoryPart.update({ where: { id: p.id }, data: { imageUrl: newUrl } });
  137. console.log(` [ok] Part ${p.id}: ${p.imageUrl} -> ${newUrl}`);
  138. totalMigrated++;
  139. } else {
  140. console.log(` [missing] Part ${p.id}: source file not found "${src}"`);
  141. await db.inventoryPart.update({ where: { id: p.id }, data: { imageUrl: newUrl } });
  142. totalSkipped++;
  143. }
  144. }
  145. // 4. Migrate AppSetting logos
  146. console.log("\n--- Migrating logo settings ---");
  147. const logos = await db.appSetting.findMany({
  148. where: { key: "workshop.logo" },
  149. select: { id: true, value: true, organizationId: true },
  150. });
  151. for (const logo of logos) {
  152. if (logo.value.startsWith("/api/files/") || !logo.organizationId) continue;
  153. const parsed = parseOldUrl(logo.value);
  154. if (!parsed) {
  155. console.log(` [skip] Logo setting ${logo.id}: unrecognized URL "${logo.value}"`);
  156. totalSkipped++;
  157. continue;
  158. }
  159. const orgId = logo.organizationId;
  160. const src = path.join(PUBLIC_UPLOADS, parsed.category, parsed.filename);
  161. const dst = path.join(DATA_UPLOADS, orgId, "logos", parsed.filename);
  162. const newUrl = `/api/files/${orgId}/logos/${parsed.filename}`;
  163. if (await fileExists(src)) {
  164. await copyFile(src, dst);
  165. await db.appSetting.update({ where: { id: logo.id }, data: { value: newUrl } });
  166. console.log(` [ok] Logo ${logo.id}: ${logo.value} -> ${newUrl}`);
  167. totalMigrated++;
  168. } else {
  169. console.log(` [missing] Logo ${logo.id}: source file not found "${src}"`);
  170. await db.appSetting.update({ where: { id: logo.id }, data: { value: newUrl } });
  171. totalSkipped++;
  172. }
  173. }
  174. console.log(`\nMigration complete! Migrated: ${totalMigrated}, Skipped: ${totalSkipped}`);
  175. console.log("\nYou can now safely remove public/uploads/ after verifying everything works.");
  176. }
  177. main()
  178. .catch((e) => {
  179. console.error("Migration failed:", e);
  180. process.exit(1);
  181. })
  182. .finally(() => db.$disconnect());