attachmentMigrationManager.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. /**
  2. * Attachment Migration Manager
  3. * Handles migration of attachments from old structure to new structure
  4. * with UI feedback and spinners for unconverted attachments
  5. */
  6. import { ReactiveVar } from 'meteor/reactive-var';
  7. import { ReactiveCache } from '/imports/reactiveCache';
  8. // Reactive variables for attachment migration progress
  9. export const attachmentMigrationProgress = new ReactiveVar(0);
  10. export const attachmentMigrationStatus = new ReactiveVar('');
  11. export const isMigratingAttachments = new ReactiveVar(false);
  12. export const unconvertedAttachments = new ReactiveVar([]);
  13. // Global tracking of migrated boards (persistent across component reinitializations)
  14. const globalMigratedBoards = new Set();
  15. class AttachmentMigrationManager {
  16. constructor() {
  17. this.migrationCache = new Map(); // Cache migrated attachment IDs
  18. this.migratedBoards = new Set(); // Track boards that have been migrated
  19. }
  20. /**
  21. * Check if an attachment needs migration
  22. * @param {string} attachmentId - The attachment ID to check
  23. * @returns {boolean} - True if attachment needs migration
  24. */
  25. needsMigration(attachmentId) {
  26. if (this.migrationCache.has(attachmentId)) {
  27. return false; // Already migrated
  28. }
  29. try {
  30. const attachment = ReactiveCache.getAttachment(attachmentId);
  31. if (!attachment) return false;
  32. // Check if attachment has old structure (no meta field or missing required fields)
  33. return !attachment.meta ||
  34. !attachment.meta.cardId ||
  35. !attachment.meta.boardId ||
  36. !attachment.meta.listId;
  37. } catch (error) {
  38. console.error('Error checking if attachment needs migration:', error);
  39. return false;
  40. }
  41. }
  42. /**
  43. * Check if a board has been migrated
  44. * @param {string} boardId - The board ID
  45. * @returns {boolean} - True if board has been migrated
  46. */
  47. isBoardMigrated(boardId) {
  48. return globalMigratedBoards.has(boardId);
  49. }
  50. /**
  51. * Check if a board has been migrated (server-side check)
  52. * @param {string} boardId - The board ID
  53. * @returns {Promise<boolean>} - True if board has been migrated
  54. */
  55. async isBoardMigratedServer(boardId) {
  56. return new Promise((resolve) => {
  57. Meteor.call('attachmentMigration.isBoardMigrated', boardId, (error, result) => {
  58. if (error) {
  59. console.error('Error checking board migration status:', error);
  60. resolve(false);
  61. } else {
  62. resolve(result);
  63. }
  64. });
  65. });
  66. }
  67. /**
  68. * Get all unconverted attachments for a board
  69. * @param {string} boardId - The board ID
  70. * @returns {Array} - Array of unconverted attachments
  71. */
  72. getUnconvertedAttachments(boardId) {
  73. try {
  74. const attachments = ReactiveCache.getAttachments({
  75. 'meta.boardId': boardId
  76. });
  77. return attachments.filter(attachment => this.needsMigration(attachment._id));
  78. } catch (error) {
  79. console.error('Error getting unconverted attachments:', error);
  80. return [];
  81. }
  82. }
  83. /**
  84. * Start migration for attachments in a board
  85. * @param {string} boardId - The board ID
  86. */
  87. async startAttachmentMigration(boardId) {
  88. if (isMigratingAttachments.get()) {
  89. return; // Already migrating
  90. }
  91. // Check if this board has already been migrated (client-side check first)
  92. if (globalMigratedBoards.has(boardId)) {
  93. console.log(`Board ${boardId} has already been migrated (client-side), skipping`);
  94. return;
  95. }
  96. // Double-check with server-side check
  97. const serverMigrated = await this.isBoardMigratedServer(boardId);
  98. if (serverMigrated) {
  99. console.log(`Board ${boardId} has already been migrated (server-side), skipping`);
  100. globalMigratedBoards.add(boardId); // Sync client-side tracking
  101. return;
  102. }
  103. isMigratingAttachments.set(true);
  104. attachmentMigrationStatus.set('Starting attachment migration...');
  105. attachmentMigrationProgress.set(0);
  106. try {
  107. const unconverted = this.getUnconvertedAttachments(boardId);
  108. unconvertedAttachments.set(unconverted);
  109. if (unconverted.length === 0) {
  110. attachmentMigrationStatus.set('All attachments are already migrated');
  111. attachmentMigrationProgress.set(100);
  112. isMigratingAttachments.set(false);
  113. globalMigratedBoards.add(boardId); // Mark board as migrated
  114. console.log(`Board ${boardId} has no attachments to migrate, marked as migrated`);
  115. return;
  116. }
  117. // Start server-side migration
  118. Meteor.call('attachmentMigration.migrateBoardAttachments', boardId, (error, result) => {
  119. if (error) {
  120. console.error('Failed to start attachment migration:', error);
  121. const errorMessage = error.message || error.reason || error.toString();
  122. attachmentMigrationStatus.set(`Migration failed: ${errorMessage}`);
  123. isMigratingAttachments.set(false);
  124. } else {
  125. console.log('Attachment migration started for board:', boardId);
  126. this.pollAttachmentMigrationProgress(boardId);
  127. }
  128. });
  129. } catch (error) {
  130. console.error('Error starting attachment migration:', error);
  131. attachmentMigrationStatus.set(`Migration failed: ${error.message}`);
  132. isMigratingAttachments.set(false);
  133. }
  134. }
  135. /**
  136. * Poll for attachment migration progress
  137. * @param {string} boardId - The board ID
  138. */
  139. pollAttachmentMigrationProgress(boardId) {
  140. const pollInterval = setInterval(() => {
  141. Meteor.call('attachmentMigration.getProgress', boardId, (error, result) => {
  142. if (error) {
  143. console.error('Error getting migration progress:', error);
  144. clearInterval(pollInterval);
  145. isMigratingAttachments.set(false);
  146. return;
  147. }
  148. if (result) {
  149. attachmentMigrationProgress.set(result.progress);
  150. attachmentMigrationStatus.set(result.status);
  151. unconvertedAttachments.set(result.unconvertedAttachments || []);
  152. // Stop polling if migration is complete
  153. if (result.progress >= 100 || result.status === 'completed') {
  154. clearInterval(pollInterval);
  155. isMigratingAttachments.set(false);
  156. this.migrationCache.clear(); // Clear cache to refresh data
  157. globalMigratedBoards.add(boardId); // Mark board as migrated
  158. console.log(`Board ${boardId} migration completed and marked as migrated`);
  159. }
  160. }
  161. });
  162. }, 1000);
  163. }
  164. /**
  165. * Check if an attachment is currently being migrated
  166. * @param {string} attachmentId - The attachment ID
  167. * @returns {boolean} - True if attachment is being migrated
  168. */
  169. isAttachmentBeingMigrated(attachmentId) {
  170. const unconverted = unconvertedAttachments.get();
  171. return unconverted.some(attachment => attachment._id === attachmentId);
  172. }
  173. /**
  174. * Get migration status for an attachment
  175. * @param {string} attachmentId - The attachment ID
  176. * @returns {string} - Migration status ('migrated', 'migrating', 'unmigrated')
  177. */
  178. getAttachmentMigrationStatus(attachmentId) {
  179. if (this.migrationCache.has(attachmentId)) {
  180. return 'migrated';
  181. }
  182. if (this.isAttachmentBeingMigrated(attachmentId)) {
  183. return 'migrating';
  184. }
  185. return this.needsMigration(attachmentId) ? 'unmigrated' : 'migrated';
  186. }
  187. }
  188. export const attachmentMigrationManager = new AttachmentMigrationManager();