| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 | /** * Attachment Migration Manager * Handles migration of attachments from old structure to new structure * with UI feedback and spinners for unconverted attachments */import { ReactiveVar } from 'meteor/reactive-var';import { ReactiveCache } from '/imports/reactiveCache';// Reactive variables for attachment migration progressexport const attachmentMigrationProgress = new ReactiveVar(0);export const attachmentMigrationStatus = new ReactiveVar('');export const isMigratingAttachments = new ReactiveVar(false);export const unconvertedAttachments = new ReactiveVar([]);// Global tracking of migrated boards (persistent across component reinitializations)const globalMigratedBoards = new Set();class AttachmentMigrationManager {  constructor() {    this.migrationCache = new Map(); // Cache migrated attachment IDs    this.migratedBoards = new Set(); // Track boards that have been migrated  }  /**   * Check if an attachment needs migration   * @param {string} attachmentId - The attachment ID to check   * @returns {boolean} - True if attachment needs migration   */  needsMigration(attachmentId) {    if (this.migrationCache.has(attachmentId)) {      return false; // Already migrated    }    try {      const attachment = ReactiveCache.getAttachment(attachmentId);      if (!attachment) return false;      // Check if attachment has old structure (no meta field or missing required fields)      return !attachment.meta ||              !attachment.meta.cardId ||              !attachment.meta.boardId ||             !attachment.meta.listId;    } catch (error) {      console.error('Error checking if attachment needs migration:', error);      return false;    }  }  /**   * Check if a board has been migrated   * @param {string} boardId - The board ID   * @returns {boolean} - True if board has been migrated   */  isBoardMigrated(boardId) {    return globalMigratedBoards.has(boardId);  }  /**   * Check if a board has been migrated (server-side check)   * @param {string} boardId - The board ID   * @returns {Promise<boolean>} - True if board has been migrated   */  async isBoardMigratedServer(boardId) {    return new Promise((resolve) => {      Meteor.call('attachmentMigration.isBoardMigrated', boardId, (error, result) => {        if (error) {          console.error('Error checking board migration status:', error);          resolve(false);        } else {          resolve(result);        }      });    });  }  /**   * Get all unconverted attachments for a board   * @param {string} boardId - The board ID   * @returns {Array} - Array of unconverted attachments   */  getUnconvertedAttachments(boardId) {    try {      const attachments = ReactiveCache.getAttachments({        'meta.boardId': boardId      });      return attachments.filter(attachment => this.needsMigration(attachment._id));    } catch (error) {      console.error('Error getting unconverted attachments:', error);      return [];    }  }  /**   * Start migration for attachments in a board   * @param {string} boardId - The board ID   */  async startAttachmentMigration(boardId) {    if (isMigratingAttachments.get()) {      return; // Already migrating    }    // Check if this board has already been migrated (client-side check first)    if (globalMigratedBoards.has(boardId)) {      if (process.env.DEBUG === 'true') {        console.log(`Board ${boardId} has already been migrated (client-side), skipping`);      }      return;    }    // Double-check with server-side check    const serverMigrated = await this.isBoardMigratedServer(boardId);    if (serverMigrated) {      if (process.env.DEBUG === 'true') {        console.log(`Board ${boardId} has already been migrated (server-side), skipping`);      }      globalMigratedBoards.add(boardId); // Sync client-side tracking      return;    }    isMigratingAttachments.set(true);    attachmentMigrationStatus.set('Starting attachment migration...');    attachmentMigrationProgress.set(0);    try {      const unconverted = this.getUnconvertedAttachments(boardId);      unconvertedAttachments.set(unconverted);      if (unconverted.length === 0) {        attachmentMigrationStatus.set('All attachments are already migrated');        attachmentMigrationProgress.set(100);        isMigratingAttachments.set(false);        globalMigratedBoards.add(boardId); // Mark board as migrated        if (process.env.DEBUG === 'true') {          console.log(`Board ${boardId} has no attachments to migrate, marked as migrated`);        }        return;      }      // Start server-side migration      Meteor.call('attachmentMigration.migrateBoardAttachments', boardId, (error, result) => {        if (error) {          console.error('Failed to start attachment migration:', error);          const errorMessage = error.message || error.reason || error.toString();          attachmentMigrationStatus.set(`Migration failed: ${errorMessage}`);          isMigratingAttachments.set(false);        } else {          if (process.env.DEBUG === 'true') {            console.log('Attachment migration started for board:', boardId);          }          this.pollAttachmentMigrationProgress(boardId);        }      });    } catch (error) {      console.error('Error starting attachment migration:', error);      attachmentMigrationStatus.set(`Migration failed: ${error.message}`);      isMigratingAttachments.set(false);    }  }  /**   * Poll for attachment migration progress   * @param {string} boardId - The board ID   */  pollAttachmentMigrationProgress(boardId) {    const pollInterval = setInterval(() => {      Meteor.call('attachmentMigration.getProgress', boardId, (error, result) => {        if (error) {          console.error('Error getting migration progress:', error);          clearInterval(pollInterval);          isMigratingAttachments.set(false);          return;        }        if (result) {          attachmentMigrationProgress.set(result.progress);          attachmentMigrationStatus.set(result.status);          unconvertedAttachments.set(result.unconvertedAttachments || []);          // Stop polling if migration is complete          if (result.progress >= 100 || result.status === 'completed') {            clearInterval(pollInterval);            isMigratingAttachments.set(false);            this.migrationCache.clear(); // Clear cache to refresh data            globalMigratedBoards.add(boardId); // Mark board as migrated            if (process.env.DEBUG === 'true') {              console.log(`Board ${boardId} migration completed and marked as migrated`);            }          }        }      });    }, 1000);  }  /**   * Check if an attachment is currently being migrated   * @param {string} attachmentId - The attachment ID   * @returns {boolean} - True if attachment is being migrated   */  isAttachmentBeingMigrated(attachmentId) {    const unconverted = unconvertedAttachments.get();    return unconverted.some(attachment => attachment._id === attachmentId);  }  /**   * Get migration status for an attachment   * @param {string} attachmentId - The attachment ID   * @returns {string} - Migration status ('migrated', 'migrating', 'unmigrated')   */  getAttachmentMigrationStatus(attachmentId) {    if (this.migrationCache.has(attachmentId)) {      return 'migrated';    }    if (this.isAttachmentBeingMigrated(attachmentId)) {      return 'migrating';    }    return this.needsMigration(attachmentId) ? 'unmigrated' : 'migrated';  }}export const attachmentMigrationManager = new AttachmentMigrationManager();
 |