attachmentBackwardCompatibility.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import { ReactiveCache } from '/imports/reactiveCache';
  2. import { Meteor } from 'meteor/meteor';
  3. import { MongoInternals } from 'meteor/mongo';
  4. /**
  5. * Backward compatibility layer for CollectionFS to Meteor-Files migration
  6. * Handles reading attachments from old CollectionFS database structure
  7. */
  8. // Old CollectionFS collections
  9. const OldAttachmentsFiles = new Mongo.Collection('cfs_gridfs.attachments.files');
  10. const OldAttachmentsFileRecord = new Mongo.Collection('cfs.attachments.filerecord');
  11. /**
  12. * Check if an attachment exists in the new Meteor-Files structure
  13. * @param {string} attachmentId - The attachment ID to check
  14. * @returns {boolean} - True if exists in new structure
  15. */
  16. export function isNewAttachmentStructure(attachmentId) {
  17. if (Meteor.isServer) {
  18. return !!ReactiveCache.getAttachment(attachmentId);
  19. }
  20. return false;
  21. }
  22. /**
  23. * Get attachment data from old CollectionFS structure
  24. * @param {string} attachmentId - The attachment ID
  25. * @returns {Object|null} - Attachment data in new format or null if not found
  26. */
  27. export function getOldAttachmentData(attachmentId) {
  28. if (Meteor.isServer) {
  29. try {
  30. // First try to get from old filerecord collection
  31. const fileRecord = OldAttachmentsFileRecord.findOne({ _id: attachmentId });
  32. if (!fileRecord) {
  33. return null;
  34. }
  35. // Get file data from old files collection
  36. const fileData = OldAttachmentsFiles.findOne({ _id: attachmentId });
  37. if (!fileData) {
  38. return null;
  39. }
  40. // Convert old structure to new structure
  41. const convertedAttachment = {
  42. _id: attachmentId,
  43. name: fileRecord.original?.name || fileData.filename || 'Unknown',
  44. size: fileRecord.original?.size || fileData.length || 0,
  45. type: fileRecord.original?.type || fileData.contentType || 'application/octet-stream',
  46. extension: getFileExtension(fileRecord.original?.name || fileData.filename || ''),
  47. extensionWithDot: getFileExtensionWithDot(fileRecord.original?.name || fileData.filename || ''),
  48. meta: {
  49. boardId: fileRecord.boardId,
  50. swimlaneId: fileRecord.swimlaneId,
  51. listId: fileRecord.listId,
  52. cardId: fileRecord.cardId,
  53. userId: fileRecord.userId,
  54. source: 'legacy'
  55. },
  56. uploadedAt: fileRecord.uploadedAt || fileData.uploadDate || new Date(),
  57. updatedAt: fileRecord.original?.updatedAt || fileData.uploadDate || new Date(),
  58. // Legacy compatibility fields
  59. isImage: isImageFile(fileRecord.original?.type || fileData.contentType),
  60. isVideo: isVideoFile(fileRecord.original?.type || fileData.contentType),
  61. isAudio: isAudioFile(fileRecord.original?.type || fileData.contentType),
  62. isText: isTextFile(fileRecord.original?.type || fileData.contentType),
  63. isJSON: isJSONFile(fileRecord.original?.type || fileData.contentType),
  64. isPDF: isPDFFile(fileRecord.original?.type || fileData.contentType),
  65. // Legacy link method for compatibility
  66. link: function(version = 'original') {
  67. return `/cfs/files/attachments/${this._id}`;
  68. },
  69. // Legacy versions structure for compatibility
  70. versions: {
  71. original: {
  72. path: `/cfs/files/attachments/${this._id}`,
  73. size: this.size,
  74. type: this.type,
  75. storage: 'gridfs'
  76. }
  77. }
  78. };
  79. return convertedAttachment;
  80. } catch (error) {
  81. console.error('Error reading old attachment data:', error);
  82. return null;
  83. }
  84. }
  85. return null;
  86. }
  87. /**
  88. * Get file extension from filename
  89. * @param {string} filename - The filename
  90. * @returns {string} - File extension without dot
  91. */
  92. function getFileExtension(filename) {
  93. if (!filename) return '';
  94. const lastDot = filename.lastIndexOf('.');
  95. if (lastDot === -1) return '';
  96. return filename.substring(lastDot + 1).toLowerCase();
  97. }
  98. /**
  99. * Get file extension with dot
  100. * @param {string} filename - The filename
  101. * @returns {string} - File extension with dot
  102. */
  103. function getFileExtensionWithDot(filename) {
  104. const ext = getFileExtension(filename);
  105. return ext ? `.${ext}` : '';
  106. }
  107. /**
  108. * Check if file is an image
  109. * @param {string} mimeType - MIME type
  110. * @returns {boolean} - True if image
  111. */
  112. function isImageFile(mimeType) {
  113. return mimeType && mimeType.startsWith('image/');
  114. }
  115. /**
  116. * Check if file is a video
  117. * @param {string} mimeType - MIME type
  118. * @returns {boolean} - True if video
  119. */
  120. function isVideoFile(mimeType) {
  121. return mimeType && mimeType.startsWith('video/');
  122. }
  123. /**
  124. * Check if file is audio
  125. * @param {string} mimeType - MIME type
  126. * @returns {boolean} - True if audio
  127. */
  128. function isAudioFile(mimeType) {
  129. return mimeType && mimeType.startsWith('audio/');
  130. }
  131. /**
  132. * Check if file is text
  133. * @param {string} mimeType - MIME type
  134. * @returns {boolean} - True if text
  135. */
  136. function isTextFile(mimeType) {
  137. return mimeType && mimeType.startsWith('text/');
  138. }
  139. /**
  140. * Check if file is JSON
  141. * @param {string} mimeType - MIME type
  142. * @returns {boolean} - True if JSON
  143. */
  144. function isJSONFile(mimeType) {
  145. return mimeType === 'application/json';
  146. }
  147. /**
  148. * Check if file is PDF
  149. * @param {string} mimeType - MIME type
  150. * @returns {boolean} - True if PDF
  151. */
  152. function isPDFFile(mimeType) {
  153. return mimeType === 'application/pdf';
  154. }
  155. /**
  156. * Get attachment with backward compatibility
  157. * @param {string} attachmentId - The attachment ID
  158. * @returns {Object|null} - Attachment data or null if not found
  159. */
  160. export function getAttachmentWithBackwardCompatibility(attachmentId) {
  161. // First try new structure
  162. if (isNewAttachmentStructure(attachmentId)) {
  163. return ReactiveCache.getAttachment(attachmentId);
  164. }
  165. // Fall back to old structure
  166. return getOldAttachmentData(attachmentId);
  167. }
  168. /**
  169. * Get attachments for a card with backward compatibility
  170. * @param {Object} query - Query object
  171. * @returns {Array} - Array of attachments
  172. */
  173. export function getAttachmentsWithBackwardCompatibility(query) {
  174. const newAttachments = ReactiveCache.getAttachments(query);
  175. const oldAttachments = [];
  176. if (Meteor.isServer) {
  177. try {
  178. // Query old structure for the same card
  179. const cardId = query['meta.cardId'];
  180. if (cardId) {
  181. const oldFileRecords = OldAttachmentsFileRecord.find({ cardId }).fetch();
  182. for (const fileRecord of oldFileRecords) {
  183. const oldAttachment = getOldAttachmentData(fileRecord._id);
  184. if (oldAttachment) {
  185. oldAttachments.push(oldAttachment);
  186. }
  187. }
  188. }
  189. } catch (error) {
  190. console.error('Error reading old attachments:', error);
  191. }
  192. }
  193. // Combine and deduplicate
  194. const allAttachments = [...newAttachments, ...oldAttachments];
  195. const uniqueAttachments = allAttachments.filter((attachment, index, self) =>
  196. index === self.findIndex(a => a._id === attachment._id)
  197. );
  198. return uniqueAttachments;
  199. }
  200. /**
  201. * Get file stream from old GridFS structure
  202. * @param {string} attachmentId - The attachment ID
  203. * @returns {Object|null} - GridFS file stream or null if not found
  204. */
  205. export function getOldAttachmentStream(attachmentId) {
  206. if (Meteor.isServer) {
  207. try {
  208. const db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;
  209. const bucket = new MongoInternals.NpmModule.GridFSBucket(db, {
  210. bucketName: 'cfs_gridfs.attachments'
  211. });
  212. const downloadStream = bucket.openDownloadStreamByName(attachmentId);
  213. return downloadStream;
  214. } catch (error) {
  215. console.error('Error creating GridFS stream:', error);
  216. return null;
  217. }
  218. }
  219. return null;
  220. }
  221. /**
  222. * Get file data from old GridFS structure
  223. * @param {string} attachmentId - The attachment ID
  224. * @returns {Buffer|null} - File data buffer or null if not found
  225. */
  226. export function getOldAttachmentDataBuffer(attachmentId) {
  227. if (Meteor.isServer) {
  228. try {
  229. const db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;
  230. const bucket = new MongoInternals.NpmModule.GridFSBucket(db, {
  231. bucketName: 'cfs_gridfs.attachments'
  232. });
  233. return new Promise((resolve, reject) => {
  234. const chunks = [];
  235. const downloadStream = bucket.openDownloadStreamByName(attachmentId);
  236. downloadStream.on('data', (chunk) => {
  237. chunks.push(chunk);
  238. });
  239. downloadStream.on('end', () => {
  240. resolve(Buffer.concat(chunks));
  241. });
  242. downloadStream.on('error', (error) => {
  243. reject(error);
  244. });
  245. });
  246. } catch (error) {
  247. console.error('Error reading GridFS data:', error);
  248. return null;
  249. }
  250. }
  251. return null;
  252. }