attachmentMigration.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. /**
  2. * Server-side Attachment Migration System
  3. * Handles migration of attachments from old structure to new structure
  4. */
  5. import { Meteor } from 'meteor/meteor';
  6. import { ReactiveVar } from 'meteor/reactive-var';
  7. import { check } from 'meteor/check';
  8. import { ReactiveCache } from '/imports/reactiveCache';
  9. import Attachments from '/models/attachments';
  10. // Reactive variables for tracking migration progress
  11. const migrationProgress = new ReactiveVar(0);
  12. const migrationStatus = new ReactiveVar('');
  13. const unconvertedAttachments = new ReactiveVar([]);
  14. // Track migrated boards on server side
  15. const migratedBoards = new Set();
  16. class AttachmentMigrationService {
  17. constructor() {
  18. this.migrationCache = new Map();
  19. }
  20. /**
  21. * Check if a board has been migrated
  22. * @param {string} boardId - The board ID
  23. * @returns {boolean} - True if board has been migrated
  24. */
  25. isBoardMigrated(boardId) {
  26. return migratedBoards.has(boardId);
  27. }
  28. /**
  29. * Migrate all attachments for a board
  30. * @param {string} boardId - The board ID
  31. */
  32. async migrateBoardAttachments(boardId) {
  33. try {
  34. // Check if board has already been migrated
  35. if (this.isBoardMigrated(boardId)) {
  36. console.log(`Board ${boardId} has already been migrated, skipping`);
  37. return { success: true, message: 'Board already migrated' };
  38. }
  39. console.log(`Starting attachment migration for board: ${boardId}`);
  40. // Get all attachments for the board
  41. const attachments = Attachments.find({
  42. 'meta.boardId': boardId
  43. }).fetch();
  44. const totalAttachments = attachments.length;
  45. let migratedCount = 0;
  46. migrationStatus.set(`Migrating ${totalAttachments} attachments...`);
  47. migrationProgress.set(0);
  48. for (const attachment of attachments) {
  49. try {
  50. // Check if attachment needs migration
  51. if (this.needsMigration(attachment)) {
  52. await this.migrateAttachment(attachment);
  53. this.migrationCache.set(attachment._id, true);
  54. }
  55. migratedCount++;
  56. const progress = Math.round((migratedCount / totalAttachments) * 100);
  57. migrationProgress.set(progress);
  58. migrationStatus.set(`Migrated ${migratedCount}/${totalAttachments} attachments...`);
  59. } catch (error) {
  60. console.error(`Error migrating attachment ${attachment._id}:`, error);
  61. }
  62. }
  63. // Update unconverted attachments list
  64. const remainingUnconverted = this.getUnconvertedAttachments(boardId);
  65. unconvertedAttachments.set(remainingUnconverted);
  66. migrationStatus.set('Attachment migration completed');
  67. migrationProgress.set(100);
  68. // Mark board as migrated
  69. migratedBoards.add(boardId);
  70. console.log(`Attachment migration completed for board: ${boardId}`);
  71. console.log(`Marked board ${boardId} as migrated`);
  72. return { success: true, message: 'Migration completed' };
  73. } catch (error) {
  74. console.error(`Error migrating attachments for board ${boardId}:`, error);
  75. migrationStatus.set(`Migration failed: ${error.message}`);
  76. throw error;
  77. }
  78. }
  79. /**
  80. * Check if an attachment needs migration
  81. * @param {Object} attachment - The attachment object
  82. * @returns {boolean} - True if attachment needs migration
  83. */
  84. needsMigration(attachment) {
  85. if (this.migrationCache.has(attachment._id)) {
  86. return false; // Already migrated
  87. }
  88. // Check if attachment has old structure
  89. return !attachment.meta ||
  90. !attachment.meta.cardId ||
  91. !attachment.meta.boardId ||
  92. !attachment.meta.listId;
  93. }
  94. /**
  95. * Migrate a single attachment
  96. * @param {Object} attachment - The attachment object
  97. */
  98. async migrateAttachment(attachment) {
  99. try {
  100. // Get the card to find board and list information
  101. const card = ReactiveCache.getCard(attachment.cardId);
  102. if (!card) {
  103. console.warn(`Card not found for attachment ${attachment._id}`);
  104. return;
  105. }
  106. const list = ReactiveCache.getList(card.listId);
  107. if (!list) {
  108. console.warn(`List not found for attachment ${attachment._id}`);
  109. return;
  110. }
  111. // Update attachment with new structure
  112. const updateData = {
  113. meta: {
  114. cardId: attachment.cardId,
  115. boardId: list.boardId,
  116. listId: card.listId,
  117. userId: attachment.userId,
  118. createdAt: attachment.createdAt || new Date(),
  119. migratedAt: new Date()
  120. }
  121. };
  122. // Preserve existing meta data if it exists
  123. if (attachment.meta) {
  124. updateData.meta = {
  125. ...attachment.meta,
  126. ...updateData.meta
  127. };
  128. }
  129. Attachments.update(attachment._id, { $set: updateData });
  130. console.log(`Migrated attachment ${attachment._id}`);
  131. } catch (error) {
  132. console.error(`Error migrating attachment ${attachment._id}:`, error);
  133. throw error;
  134. }
  135. }
  136. /**
  137. * Get unconverted attachments for a board
  138. * @param {string} boardId - The board ID
  139. * @returns {Array} - Array of unconverted attachments
  140. */
  141. getUnconvertedAttachments(boardId) {
  142. try {
  143. const attachments = Attachments.find({
  144. 'meta.boardId': boardId
  145. }).fetch();
  146. return attachments.filter(attachment => this.needsMigration(attachment));
  147. } catch (error) {
  148. console.error('Error getting unconverted attachments:', error);
  149. return [];
  150. }
  151. }
  152. /**
  153. * Get migration progress
  154. * @param {string} boardId - The board ID
  155. * @returns {Object} - Migration progress data
  156. */
  157. getMigrationProgress(boardId) {
  158. const progress = migrationProgress.get();
  159. const status = migrationStatus.get();
  160. const unconverted = this.getUnconvertedAttachments(boardId);
  161. return {
  162. progress,
  163. status,
  164. unconvertedAttachments: unconverted
  165. };
  166. }
  167. }
  168. const attachmentMigrationService = new AttachmentMigrationService();
  169. // Meteor methods
  170. Meteor.methods({
  171. async 'attachmentMigration.migrateBoardAttachments'(boardId) {
  172. check(boardId, String);
  173. if (!this.userId) {
  174. throw new Meteor.Error('not-authorized');
  175. }
  176. return await attachmentMigrationService.migrateBoardAttachments(boardId);
  177. },
  178. 'attachmentMigration.getProgress'(boardId) {
  179. check(boardId, String);
  180. if (!this.userId) {
  181. throw new Meteor.Error('not-authorized');
  182. }
  183. return attachmentMigrationService.getMigrationProgress(boardId);
  184. },
  185. 'attachmentMigration.getUnconvertedAttachments'(boardId) {
  186. check(boardId, String);
  187. if (!this.userId) {
  188. throw new Meteor.Error('not-authorized');
  189. }
  190. return attachmentMigrationService.getUnconvertedAttachments(boardId);
  191. },
  192. 'attachmentMigration.isBoardMigrated'(boardId) {
  193. check(boardId, String);
  194. if (!this.userId) {
  195. throw new Meteor.Error('not-authorized');
  196. }
  197. return attachmentMigrationService.isBoardMigrated(boardId);
  198. }
  199. });
  200. export { attachmentMigrationService };