From b9204254bde3b4ff3e689a0dc01ecf970fef8a10 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Thu, 22 Jan 2026 10:53:44 -0300 Subject: [PATCH 1/2] fix: support multiple file exports in room message export --- .../dataExport/exportRoomMessagesToFile.ts | 25 +++++---- .../exportRoomMessagesToFile.spec.ts | 56 ++++++++++++++++++- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/apps/meteor/server/lib/dataExport/exportRoomMessagesToFile.ts b/apps/meteor/server/lib/dataExport/exportRoomMessagesToFile.ts index 9dff1771cc877..76befbd38b004 100644 --- a/apps/meteor/server/lib/dataExport/exportRoomMessagesToFile.ts +++ b/apps/meteor/server/lib/dataExport/exportRoomMessagesToFile.ts @@ -163,7 +163,7 @@ export const getMessageData = ( return messageObject; }; -export const exportMessageObject = (type: 'json' | 'html', messageObject: MessageData, messageFile?: FileProp): string => { +export const exportMessageObject = (type: 'json' | 'html', messageObject: MessageData, messageFiles: FileProp[] = []): string => { if (type === 'json') { return JSON.stringify(messageObject); } @@ -180,14 +180,16 @@ export const exportMessageObject = (type: 'json' | 'html', messageObject: Messag file.push(`

${messageObject.username} (${timestamp}):
`); file.push(message); - if (messageFile?._id) { - const attachment = messageObject.attachments?.find((att) => att.type === 'file' && att.title_link?.includes(messageFile._id)); + for (const messageFile of messageFiles) { + if (messageFile?._id) { + const attachment = messageObject.attachments?.find((att) => att.type === 'file' && att.title_link?.includes(messageFile._id)); - const description = attachment?.title || i18n.t('Message_Attachments'); + const description = attachment?.title || i18n.t('Message_Attachments'); - const assetUrl = `./assets/${messageFile._id}-${messageFile.name}`; - const link = `
${description}`; - file.push(link); + const assetUrl = `./assets/${messageFile._id}-${messageFile.name}`; + const link = `
${description}`; + file.push(link); + } } file.push('

'); @@ -229,11 +231,12 @@ export const exportRoomMessages = async ( results.forEach((msg) => { const messageObject = getMessageData(msg, hideUsers, userData, usersMap); - if (msg.file) { - result.uploads.push(msg.file); - } + // handle both new format (msg.files array) and old format (msg.file) for backward compatibility + // and filter out thumbnails (typeGroup === 'thumb') to only include actual files + const files = (msg.files || (msg.file ? [msg.file] : [])).filter((file) => file && file.typeGroup !== 'thumb'); - result.messages.push(exportMessageObject(exportType, messageObject, msg.file)); + result.uploads.push(...files); + result.messages.push(exportMessageObject(exportType, messageObject, files)); }); return result; diff --git a/apps/meteor/tests/unit/server/lib/dataExport/exportRoomMessagesToFile.spec.ts b/apps/meteor/tests/unit/server/lib/dataExport/exportRoomMessagesToFile.spec.ts index 6fb15a6802823..5fb3696f34fc1 100644 --- a/apps/meteor/tests/unit/server/lib/dataExport/exportRoomMessagesToFile.spec.ts +++ b/apps/meteor/tests/unit/server/lib/dataExport/exportRoomMessagesToFile.spec.ts @@ -87,7 +87,7 @@ describe('Export - exportMessageObject', () => { }); it('should correctly reference file when exporting a message object with an attachment as html', async () => { - const result = await exportMessageObject('html', messagesData[1], exportMessagesMock[1].file); + const result = await exportMessageObject('html', messagesData[1], [exportMessagesMock[1].file]); expect(result).to.be.a.string; expect(result).to.equal( @@ -100,7 +100,7 @@ describe('Export - exportMessageObject', () => { }); it('should use fallback attachment description when no title is provided on message object export as html', async () => { - const result = await exportMessageObject('html', messagesData[2], exportMessagesMock[2].file); + const result = await exportMessageObject('html', messagesData[2], [exportMessagesMock[2].file]); expect(stubs.translateKey.calledWith('Message_Attachments')).to.be.true; expect(result).to.be.a.string; @@ -155,4 +155,56 @@ describe('Export - exportRoomMessages', () => { const messagesWithFiles = exportMessagesMock.filter((message) => message.file); expect(result).to.have.property('uploads').that.is.an('array').of.length(messagesWithFiles.length); }); + + it('should export multiple files and filter out thumbnails', async () => { + const message = { + _id: faker.database.mongodbObjectId(), + rid: 'test-rid', + ts: new Date(), + u: { _id: faker.database.mongodbObjectId(), username: 'testuser' }, + msg: 'Message with files', + files: [ + { _id: 'file-1', name: 'photo.jpg', type: 'image/jpeg', size: 500000 }, + { _id: 'file-2', name: 'doc.pdf', type: 'application/pdf', size: 10000 }, + { _id: 'thumb-1', name: 'photo_thumb.jpg', type: 'image/jpeg', size: 5000, typeGroup: 'thumb' }, + ], + attachments: [{ type: 'file', title: 'photo.jpg', title_link: '/file-upload/file-1/photo.jpg' }], + }; + + stubs.findPaginatedMessagesCursor.resolves([message]); + stubs.findPaginatedMessagesTotal.resolves(1); + stubs.findPaginatedMessages.returns({ + cursor: { toArray: stubs.findPaginatedMessagesCursor }, + totalCount: stubs.findPaginatedMessagesTotal(), + }); + + const result = await exportRoomMessages('test-rid', 'html', 0, 100, userData); + + expect(result.uploads).to.have.length(2); + expect(result.uploads.some((f: { typeGroup?: string }) => f.typeGroup === 'thumb')).to.be.false; + }); + + it('should fallback to msg.file when msg.files is not available', async () => { + const message = { + _id: faker.database.mongodbObjectId(), + rid: 'test-rid', + ts: new Date(), + u: { _id: faker.database.mongodbObjectId(), username: 'testuser' }, + msg: 'Old format message', + file: { _id: 'single-file', name: 'doc.pdf', type: 'application/pdf', size: 10000 }, + attachments: [{ type: 'file', title: 'doc.pdf', title_link: '/file-upload/single-file/doc.pdf' }], + }; + + stubs.findPaginatedMessagesCursor.resolves([message]); + stubs.findPaginatedMessagesTotal.resolves(1); + stubs.findPaginatedMessages.returns({ + cursor: { toArray: stubs.findPaginatedMessagesCursor }, + totalCount: stubs.findPaginatedMessagesTotal(), + }); + + const result = await exportRoomMessages('test-rid', 'html', 0, 100, userData); + + expect(result.uploads).to.have.length(1); + expect(result.uploads[0]._id).to.equal('single-file'); + }); }); From 73f490767b789196b58bb942d8f703e4eabbc80c Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Thu, 22 Jan 2026 12:28:28 -0300 Subject: [PATCH 2/2] add changeset --- .changeset/quick-schools-hear.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/quick-schools-hear.md diff --git a/.changeset/quick-schools-hear.md b/.changeset/quick-schools-hear.md new file mode 100644 index 0000000000000..91e2069b81a19 --- /dev/null +++ b/.changeset/quick-schools-hear.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes room message export to correctly handle messages with multiple files.