attachmentMigrationManager.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  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. if (process.env.DEBUG === 'true') {
  94. console.log(`Board ${boardId} has already been migrated (client-side), skipping`);
  95. }
  96. return;
  97. }
  98. // Double-check with server-side check
  99. const serverMigrated = await this.isBoardMigratedServer(boardId);
  100. if (serverMigrated) {
  101. if (process.env.DEBUG === 'true') {
  102. console.log(`Board ${boardId} has already been migrated (server-side), skipping`);
  103. }
  104. globalMigratedBoards.add(boardId); // Sync client-side tracking
  105. return;
  106. }
  107. isMigratingAttachments.set(true);
  108. attachmentMigrationStatus.set('Starting attachment migration...');
  109. attachmentMigrationProgress.set(0);
  110. try {
  111. const unconverted = this.getUnconvertedAttachments(boardId);
  112. unconvertedAttachments.set(unconverted);
  113. if (unconverted.length === 0) {
  114. attachmentMigrationStatus.set('All attachments are already migrated');
  115. attachmentMigrationProgress.set(100);
  116. isMigratingAttachments.set(false);
  117. globalMigratedBoards.add(boardId); // Mark board as migrated
  118. if (process.env.DEBUG === 'true') {
  119. console.log(`Board ${boardId} has no attachments to migrate, marked as migrated`);
  120. }
  121. return;
  122. }
  123. // Start server-side migration
  124. Meteor.call('attachmentMigration.migrateBoardAttachments', boardId, (error, result) => {
  125. if (error) {
  126. console.error('Failed to start attachment migration:', error);
  127. const errorMessage = error.message || error.reason || error.toString();
  128. attachmentMigrationStatus.set(`Migration failed: ${errorMessage}`);
  129. isMigratingAttachments.set(false);
  130. } else {
  131. if (process.env.DEBUG === 'true') {
  132. console.log('Attachment migration started for board:', boardId);
  133. }
  134. this.pollAttachmentMigrationProgress(boardId);
  135. }
  136. });
  137. } catch (error) {
  138. console.error('Error starting attachment migration:', error);
  139. attachmentMigrationStatus.set(`Migration failed: ${error.message}`);
  140. isMigratingAttachments.set(false);
  141. }
  142. }
  143. /**
  144. * Poll for attachment migration progress
  145. * @param {string} boardId - The board ID
  146. */
  147. pollAttachmentMigrationProgress(boardId) {
  148. const pollInterval = setInterval(() => {
  149. Meteor.call('attachmentMigration.getProgress', boardId, (error, result) => {
  150. if (error) {
  151. console.error('Error getting migration progress:', error);
  152. clearInterval(pollInterval);
  153. isMigratingAttachments.set(false);
  154. return;
  155. }
  156. if (result) {
  157. attachmentMigrationProgress.set(result.progress);
  158. attachmentMigrationStatus.set(result.status);
  159. unconvertedAttachments.set(result.unconvertedAttachments || []);
  160. // Stop polling if migration is complete
  161. if (result.progress >= 100 || result.status === 'completed') {
  162. clearInterval(pollInterval);
  163. isMigratingAttachments.set(false);
  164. this.migrationCache.clear(); // Clear cache to refresh data
  165. globalMigratedBoards.add(boardId); // Mark board as migrated
  166. if (process.env.DEBUG === 'true') {
  167. console.log(`Board ${boardId} migration completed and marked as migrated`);
  168. }
  169. }
  170. }
  171. });
  172. }, 1000);
  173. }
  174. /**
  175. * Check if an attachment is currently being migrated
  176. * @param {string} attachmentId - The attachment ID
  177. * @returns {boolean} - True if attachment is being migrated
  178. */
  179. isAttachmentBeingMigrated(attachmentId) {
  180. const unconverted = unconvertedAttachments.get();
  181. return unconverted.some(attachment => attachment._id === attachmentId);
  182. }
  183. /**
  184. * Get migration status for an attachment
  185. * @param {string} attachmentId - The attachment ID
  186. * @returns {string} - Migration status ('migrated', 'migrating', 'unmigrated')
  187. */
  188. getAttachmentMigrationStatus(attachmentId) {
  189. if (this.migrationCache.has(attachmentId)) {
  190. return 'migrated';
  191. }
  192. if (this.isAttachmentBeingMigrated(attachmentId)) {
  193. return 'migrating';
  194. }
  195. return this.needsMigration(attachmentId) ? 'unmigrated' : 'migrated';
  196. }
  197. }
  198. export const attachmentMigrationManager = new AttachmentMigrationManager();