/** * Server-side Attachment Migration System * Handles migration of attachments from old structure to new structure */ import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { ReactiveCache } from '/imports/reactiveCache'; // Reactive variables for tracking migration progress const migrationProgress = new ReactiveVar(0); const migrationStatus = new ReactiveVar(''); const unconvertedAttachments = new ReactiveVar([]); class AttachmentMigrationService { constructor() { this.migrationCache = new Map(); } /** * Migrate all attachments for a board * @param {string} boardId - The board ID */ async migrateBoardAttachments(boardId) { try { console.log(`Starting attachment migration for board: ${boardId}`); // Get all attachments for the board const attachments = Attachments.find({ 'meta.boardId': boardId }).fetch(); const totalAttachments = attachments.length; let migratedCount = 0; migrationStatus.set(`Migrating ${totalAttachments} attachments...`); migrationProgress.set(0); for (const attachment of attachments) { try { // Check if attachment needs migration if (this.needsMigration(attachment)) { await this.migrateAttachment(attachment); this.migrationCache.set(attachment._id, true); } migratedCount++; const progress = Math.round((migratedCount / totalAttachments) * 100); migrationProgress.set(progress); migrationStatus.set(`Migrated ${migratedCount}/${totalAttachments} attachments...`); } catch (error) { console.error(`Error migrating attachment ${attachment._id}:`, error); } } // Update unconverted attachments list const remainingUnconverted = this.getUnconvertedAttachments(boardId); unconvertedAttachments.set(remainingUnconverted); migrationStatus.set('Attachment migration completed'); migrationProgress.set(100); console.log(`Attachment migration completed for board: ${boardId}`); } catch (error) { console.error(`Error migrating attachments for board ${boardId}:`, error); migrationStatus.set(`Migration failed: ${error.message}`); throw error; } } /** * Check if an attachment needs migration * @param {Object} attachment - The attachment object * @returns {boolean} - True if attachment needs migration */ needsMigration(attachment) { if (this.migrationCache.has(attachment._id)) { return false; // Already migrated } // Check if attachment has old structure return !attachment.meta || !attachment.meta.cardId || !attachment.meta.boardId || !attachment.meta.listId; } /** * Migrate a single attachment * @param {Object} attachment - The attachment object */ async migrateAttachment(attachment) { try { // Get the card to find board and list information const card = ReactiveCache.getCard(attachment.cardId); if (!card) { console.warn(`Card not found for attachment ${attachment._id}`); return; } const list = ReactiveCache.getList(card.listId); if (!list) { console.warn(`List not found for attachment ${attachment._id}`); return; } // Update attachment with new structure const updateData = { meta: { cardId: attachment.cardId, boardId: list.boardId, listId: card.listId, userId: attachment.userId, createdAt: attachment.createdAt || new Date(), migratedAt: new Date() } }; // Preserve existing meta data if it exists if (attachment.meta) { updateData.meta = { ...attachment.meta, ...updateData.meta }; } Attachments.update(attachment._id, { $set: updateData }); console.log(`Migrated attachment ${attachment._id}`); } catch (error) { console.error(`Error migrating attachment ${attachment._id}:`, error); throw error; } } /** * Get unconverted attachments for a board * @param {string} boardId - The board ID * @returns {Array} - Array of unconverted attachments */ getUnconvertedAttachments(boardId) { try { const attachments = Attachments.find({ 'meta.boardId': boardId }).fetch(); return attachments.filter(attachment => this.needsMigration(attachment)); } catch (error) { console.error('Error getting unconverted attachments:', error); return []; } } /** * Get migration progress * @param {string} boardId - The board ID * @returns {Object} - Migration progress data */ getMigrationProgress(boardId) { const progress = migrationProgress.get(); const status = migrationStatus.get(); const unconverted = this.getUnconvertedAttachments(boardId); return { progress, status, unconvertedAttachments: unconverted }; } } const attachmentMigrationService = new AttachmentMigrationService(); // Meteor methods Meteor.methods({ 'attachmentMigration.migrateBoardAttachments'(boardId) { if (!this.userId) { throw new Meteor.Error('not-authorized'); } return attachmentMigrationService.migrateBoardAttachments(boardId); }, 'attachmentMigration.getProgress'(boardId) { if (!this.userId) { throw new Meteor.Error('not-authorized'); } return attachmentMigrationService.getMigrationProgress(boardId); }, 'attachmentMigration.getUnconvertedAttachments'(boardId) { if (!this.userId) { throw new Meteor.Error('not-authorized'); } return attachmentMigrationService.getUnconvertedAttachments(boardId); } }); export { attachmentMigrationService };