Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quick-schools-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Fixes room message export to correctly handle messages with multiple files.
25 changes: 14 additions & 11 deletions apps/meteor/server/lib/dataExport/exportRoomMessagesToFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -180,14 +180,16 @@ export const exportMessageObject = (type: 'json' | 'html', messageObject: Messag
file.push(`<p><strong>${messageObject.username}</strong> (${timestamp}):<br/>`);
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 = `<br/><a href="${assetUrl}">${description}</a>`;
file.push(link);
const assetUrl = `./assets/${messageFile._id}-${messageFile.name}`;
const link = `<br/><a href="${assetUrl}">${description}</a>`;
file.push(link);
}
}

file.push('</p>');
Expand Down Expand Up @@ -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));
Comment on lines +234 to +239
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid dropping legacy msg.file when msg.files is empty.
If msg.files is an empty array but msg.file exists, the current fallback skips the legacy file. Consider falling back when the array is empty to prevent missing attachments.

🛠️ Suggested fix
-		const files = (msg.files || (msg.file ? [msg.file] : [])).filter((file) => file && file.typeGroup !== 'thumb');
+		const filesSource = msg.files && msg.files.length ? msg.files : msg.file ? [msg.file] : [];
+		const files = filesSource.filter((file) => file && file.typeGroup !== 'thumb');
🤖 Prompt for AI Agents
In `@apps/meteor/server/lib/dataExport/exportRoomMessagesToFile.ts` around lines
234 - 239, The current fallback logic for files drops a legacy msg.file when
msg.files exists but is empty; change the selection so that if msg.files is
absent or an empty array you fall back to msg.file (e.g., use msg.files only
when it is a non-empty array, otherwise use [msg.file] if present) before
applying the thumbnail filter and then push to result.uploads and call
exportMessageObject; update the files assignment near the existing files const
and keep using result.uploads.push(...files) and
result.messages.push(exportMessageObject(exportType, messageObject, files)).

});

return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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;
Expand Down Expand Up @@ -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');
});
});
Loading