| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468 | import { Meteor } from 'meteor/meteor';import { ReactiveCache } from '/imports/reactiveCache';import { Attachments, fileStoreStrategyFactory } from '/models/attachments';import { moveToStorage } from '/models/lib/fileStoreStrategy';import { STORAGE_NAME_FILESYSTEM, STORAGE_NAME_GRIDFS, STORAGE_NAME_S3 } from '/models/lib/fileStoreStrategy';import AttachmentStorageSettings from '/models/attachmentStorageSettings';import fs from 'fs';import path from 'path';import { ObjectID } from 'bson';// Attachment API methodsif (Meteor.isServer) {  Meteor.methods({    // Upload attachment via API    'api.attachment.upload'(boardId, swimlaneId, listId, cardId, fileData, fileName, fileType, storageBackend) {      if (!this.userId) {        throw new Meteor.Error('not-authorized', 'Must be logged in');      }      // Validate parameters      if (!boardId || !swimlaneId || !listId || !cardId || !fileData || !fileName) {        throw new Meteor.Error('invalid-parameters', 'Missing required parameters');      }      // Check if user has permission to modify the card      const card = ReactiveCache.getCard(cardId);      if (!card) {        throw new Meteor.Error('card-not-found', 'Card not found');      }      const board = ReactiveCache.getBoard(boardId);      if (!board) {        throw new Meteor.Error('board-not-found', 'Board not found');      }      // Check permissions      if (!board.isBoardMember(this.userId)) {        throw new Meteor.Error('not-authorized', 'You do not have permission to modify this card');      }      // Check if board allows attachments      if (!board.allowsAttachments) {        throw new Meteor.Error('attachments-not-allowed', 'Attachments are not allowed on this board');      }      // Get default storage backend if not specified      let targetStorage = storageBackend;      if (!targetStorage) {        try {          const settings = AttachmentStorageSettings.findOne({});          targetStorage = settings ? settings.getDefaultStorage() : STORAGE_NAME_FILESYSTEM;        } catch (error) {          targetStorage = STORAGE_NAME_FILESYSTEM;        }      }      // Validate storage backend      if (![STORAGE_NAME_FILESYSTEM, STORAGE_NAME_GRIDFS, STORAGE_NAME_S3].includes(targetStorage)) {        throw new Meteor.Error('invalid-storage', 'Invalid storage backend');      }      try {        // Create file object from base64 data        const fileBuffer = Buffer.from(fileData, 'base64');        const file = new File([fileBuffer], fileName, { type: fileType || 'application/octet-stream' });        // Create attachment metadata        const fileId = new ObjectID().toString();        const meta = {          boardId: boardId,          swimlaneId: swimlaneId,          listId: listId,          cardId: cardId,          fileId: fileId,          source: 'api',          storageBackend: targetStorage        };        // Create attachment        const uploader = Attachments.insert({          file: file,          meta: meta,          isBase64: false,          transport: 'http'        });        if (uploader) {          // Move to target storage if not filesystem          if (targetStorage !== STORAGE_NAME_FILESYSTEM) {            Meteor.defer(() => {              try {                moveToStorage(uploader, targetStorage, fileStoreStrategyFactory);              } catch (error) {                console.error('Error moving attachment to target storage:', error);              }            });          }          return {            success: true,            attachmentId: uploader._id,            fileName: fileName,            fileSize: fileBuffer.length,            storageBackend: targetStorage,            message: 'Attachment uploaded successfully'          };        } else {          throw new Meteor.Error('upload-failed', 'Failed to upload attachment');        }      } catch (error) {        console.error('API attachment upload error:', error);        throw new Meteor.Error('upload-error', error.message);      }    },    // Download attachment via API    'api.attachment.download'(attachmentId) {      if (!this.userId) {        throw new Meteor.Error('not-authorized', 'Must be logged in');      }      // Get attachment      const attachment = ReactiveCache.getAttachment(attachmentId);      if (!attachment) {        throw new Meteor.Error('attachment-not-found', 'Attachment not found');      }      // Check permissions      const board = ReactiveCache.getBoard(attachment.meta.boardId);      if (!board || !board.isBoardMember(this.userId)) {        throw new Meteor.Error('not-authorized', 'You do not have permission to access this attachment');      }      try {        // Get file strategy        const strategy = fileStoreStrategyFactory.getFileStrategy(attachment, 'original');        const readStream = strategy.getReadStream();        if (!readStream) {          throw new Meteor.Error('file-not-found', 'File not found in storage');        }        // Read file data        const chunks = [];        return new Promise((resolve, reject) => {          readStream.on('data', (chunk) => {            chunks.push(chunk);          });          readStream.on('end', () => {            const fileBuffer = Buffer.concat(chunks);            const base64Data = fileBuffer.toString('base64');                        resolve({              success: true,              attachmentId: attachmentId,              fileName: attachment.name,              fileSize: attachment.size,              fileType: attachment.type,              base64Data: base64Data,              storageBackend: strategy.getStorageName()            });          });          readStream.on('error', (error) => {            reject(new Meteor.Error('download-error', error.message));          });        });      } catch (error) {        console.error('API attachment download error:', error);        throw new Meteor.Error('download-error', error.message);      }    },    // List attachments for board, swimlane, list, or card    'api.attachment.list'(boardId, swimlaneId, listId, cardId) {      if (!this.userId) {        throw new Meteor.Error('not-authorized', 'Must be logged in');      }      // Check permissions      const board = ReactiveCache.getBoard(boardId);      if (!board || !board.isBoardMember(this.userId)) {        throw new Meteor.Error('not-authorized', 'You do not have permission to access this board');      }      try {        let query = { 'meta.boardId': boardId };        if (swimlaneId) {          query['meta.swimlaneId'] = swimlaneId;        }        if (listId) {          query['meta.listId'] = listId;        }        if (cardId) {          query['meta.cardId'] = cardId;        }        const attachments = ReactiveCache.getAttachments(query);                const attachmentList = attachments.map(attachment => {          const strategy = fileStoreStrategyFactory.getFileStrategy(attachment, 'original');          return {            attachmentId: attachment._id,            fileName: attachment.name,            fileSize: attachment.size,            fileType: attachment.type,            storageBackend: strategy.getStorageName(),            boardId: attachment.meta.boardId,            swimlaneId: attachment.meta.swimlaneId,            listId: attachment.meta.listId,            cardId: attachment.meta.cardId,            createdAt: attachment.uploadedAt,            isImage: attachment.isImage          };        });        return {          success: true,          attachments: attachmentList,          count: attachmentList.length        };      } catch (error) {        console.error('API attachment list error:', error);        throw new Meteor.Error('list-error', error.message);      }    },    // Copy attachment to another card    'api.attachment.copy'(attachmentId, targetBoardId, targetSwimlaneId, targetListId, targetCardId) {      if (!this.userId) {        throw new Meteor.Error('not-authorized', 'Must be logged in');      }      // Get source attachment      const sourceAttachment = ReactiveCache.getAttachment(attachmentId);      if (!sourceAttachment) {        throw new Meteor.Error('attachment-not-found', 'Source attachment not found');      }      // Check source permissions      const sourceBoard = ReactiveCache.getBoard(sourceAttachment.meta.boardId);      if (!sourceBoard || !sourceBoard.isBoardMember(this.userId)) {        throw new Meteor.Error('not-authorized', 'You do not have permission to access the source attachment');      }      // Check target permissions      const targetBoard = ReactiveCache.getBoard(targetBoardId);      if (!targetBoard || !targetBoard.isBoardMember(this.userId)) {        throw new Meteor.Error('not-authorized', 'You do not have permission to modify the target card');      }      // Check if target board allows attachments      if (!targetBoard.allowsAttachments) {        throw new Meteor.Error('attachments-not-allowed', 'Attachments are not allowed on the target board');      }      try {        // Get source file strategy        const sourceStrategy = fileStoreStrategyFactory.getFileStrategy(sourceAttachment, 'original');        const readStream = sourceStrategy.getReadStream();        if (!readStream) {          throw new Meteor.Error('file-not-found', 'Source file not found in storage');        }        // Read source file data        const chunks = [];        return new Promise((resolve, reject) => {          readStream.on('data', (chunk) => {            chunks.push(chunk);          });          readStream.on('end', () => {            try {              const fileBuffer = Buffer.concat(chunks);              const file = new File([fileBuffer], sourceAttachment.name, { type: sourceAttachment.type });              // Create new attachment metadata              const fileId = new ObjectID().toString();              const meta = {                boardId: targetBoardId,                swimlaneId: targetSwimlaneId,                listId: targetListId,                cardId: targetCardId,                fileId: fileId,                source: 'api-copy',                copyFrom: attachmentId,                copyStorage: sourceStrategy.getStorageName()              };              // Create new attachment              const uploader = Attachments.insert({                file: file,                meta: meta,                isBase64: false,                transport: 'http'              });              if (uploader) {                resolve({                  success: true,                  sourceAttachmentId: attachmentId,                  newAttachmentId: uploader._id,                  fileName: sourceAttachment.name,                  fileSize: sourceAttachment.size,                  message: 'Attachment copied successfully'                });              } else {                reject(new Meteor.Error('copy-failed', 'Failed to copy attachment'));              }            } catch (error) {              reject(new Meteor.Error('copy-error', error.message));            }          });          readStream.on('error', (error) => {            reject(new Meteor.Error('copy-error', error.message));          });        });      } catch (error) {        console.error('API attachment copy error:', error);        throw new Meteor.Error('copy-error', error.message);      }    },    // Move attachment to another card    'api.attachment.move'(attachmentId, targetBoardId, targetSwimlaneId, targetListId, targetCardId) {      if (!this.userId) {        throw new Meteor.Error('not-authorized', 'Must be logged in');      }      // Get source attachment      const sourceAttachment = ReactiveCache.getAttachment(attachmentId);      if (!sourceAttachment) {        throw new Meteor.Error('attachment-not-found', 'Source attachment not found');      }      // Check source permissions      const sourceBoard = ReactiveCache.getBoard(sourceAttachment.meta.boardId);      if (!sourceBoard || !sourceBoard.isBoardMember(this.userId)) {        throw new Meteor.Error('not-authorized', 'You do not have permission to access the source attachment');      }      // Check target permissions      const targetBoard = ReactiveCache.getBoard(targetBoardId);      if (!targetBoard || !targetBoard.isBoardMember(this.userId)) {        throw new Meteor.Error('not-authorized', 'You do not have permission to modify the target card');      }      // Check if target board allows attachments      if (!targetBoard.allowsAttachments) {        throw new Meteor.Error('attachments-not-allowed', 'Attachments are not allowed on the target board');      }      try {        // Update attachment metadata        Attachments.update(attachmentId, {          $set: {            'meta.boardId': targetBoardId,            'meta.swimlaneId': targetSwimlaneId,            'meta.listId': targetListId,            'meta.cardId': targetCardId,            'meta.source': 'api-move',            'meta.movedAt': new Date()          }        });        return {          success: true,          attachmentId: attachmentId,          fileName: sourceAttachment.name,          fileSize: sourceAttachment.size,          sourceBoardId: sourceAttachment.meta.boardId,          targetBoardId: targetBoardId,          message: 'Attachment moved successfully'        };      } catch (error) {        console.error('API attachment move error:', error);        throw new Meteor.Error('move-error', error.message);      }    },    // Delete attachment via API    'api.attachment.delete'(attachmentId) {      if (!this.userId) {        throw new Meteor.Error('not-authorized', 'Must be logged in');      }      // Get attachment      const attachment = ReactiveCache.getAttachment(attachmentId);      if (!attachment) {        throw new Meteor.Error('attachment-not-found', 'Attachment not found');      }      // Check permissions      const board = ReactiveCache.getBoard(attachment.meta.boardId);      if (!board || !board.isBoardMember(this.userId)) {        throw new Meteor.Error('not-authorized', 'You do not have permission to delete this attachment');      }      try {        // Delete attachment        Attachments.remove(attachmentId);        return {          success: true,          attachmentId: attachmentId,          fileName: attachment.name,          message: 'Attachment deleted successfully'        };      } catch (error) {        console.error('API attachment delete error:', error);        throw new Meteor.Error('delete-error', error.message);      }    },    // Get attachment info via API    'api.attachment.info'(attachmentId) {      if (!this.userId) {        throw new Meteor.Error('not-authorized', 'Must be logged in');      }      // Get attachment      const attachment = ReactiveCache.getAttachment(attachmentId);      if (!attachment) {        throw new Meteor.Error('attachment-not-found', 'Attachment not found');      }      // Check permissions      const board = ReactiveCache.getBoard(attachment.meta.boardId);      if (!board || !board.isBoardMember(this.userId)) {        throw new Meteor.Error('not-authorized', 'You do not have permission to access this attachment');      }      try {        const strategy = fileStoreStrategyFactory.getFileStrategy(attachment, 'original');                return {          success: true,          attachmentId: attachment._id,          fileName: attachment.name,          fileSize: attachment.size,          fileType: attachment.type,          storageBackend: strategy.getStorageName(),          boardId: attachment.meta.boardId,          swimlaneId: attachment.meta.swimlaneId,          listId: attachment.meta.listId,          cardId: attachment.meta.cardId,          createdAt: attachment.uploadedAt,          isImage: attachment.isImage,          versions: Object.keys(attachment.versions).map(versionName => ({            versionName: versionName,            storage: attachment.versions[versionName].storage,            size: attachment.versions[versionName].size,            type: attachment.versions[versionName].type          }))        };      } catch (error) {        console.error('API attachment info error:', error);        throw new Meteor.Error('info-error', error.message);      }    }  });}
 |