fix: Bug - Sending files between users doesn't really work

Stream oversized generic attachments to disk instead of silently dropping chunks, avoid loading completed file downloads into renderer memory, and surface a clear error when the browser client cannot receive a file.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-13 21:50:21 +02:00
parent 924d4bbb1d
commit 95259e8943
7 changed files with 187 additions and 9 deletions

View File

@@ -1,7 +1,9 @@
import {
getWatchedAttachmentRoomIdFromUrl,
isDirectMessageAttachmentRoomId,
shouldCopyUploaderMediaToAppData
shouldCopyUploaderMediaToAppData,
shouldStreamAttachmentReceiveToDisk,
canReceiveAttachment
} from './attachment.logic';
describe('attachment logic', () => {
@@ -44,4 +46,36 @@ describe('attachment logic', () => {
mime: 'video/mp4'
}, undefined, true)).toBe(false);
});
it('streams oversized generic files to disk when the store supports it', () => {
const capabilities = {
canStreamToDisk: true,
canPersistSize: (bytes: number) => bytes <= 256 * 1024 * 1024
};
expect(shouldStreamAttachmentReceiveToDisk({
size: 200 * 1024 * 1024,
mime: 'application/zip',
filePath: undefined
}, capabilities)).toBe(true);
});
it('receives browser-sized files in memory when disk streaming is unavailable', () => {
const browserCapabilities = {
canStreamToDisk: false,
canPersistSize: (bytes: number) => bytes <= 50 * 1024 * 1024
};
expect(canReceiveAttachment({
size: 20 * 1024 * 1024,
mime: 'application/zip',
filePath: undefined
}, browserCapabilities)).toBe(true);
expect(canReceiveAttachment({
size: 200 * 1024 * 1024,
mime: 'application/zip',
filePath: undefined
}, browserCapabilities)).toBe(false);
});
});

View File

@@ -50,6 +50,49 @@ export function isDirectMessageAttachmentRoomId(roomId: string | null | undefine
return !!roomId && roomId.startsWith(DIRECT_MESSAGE_ATTACHMENT_STORAGE_PREFIX);
}
export interface AttachmentReceiveCapabilities {
canStreamToDisk: boolean;
canPersistSize: (bytes: number) => boolean;
}
export function shouldStreamAttachmentReceiveToDisk(
attachment: Pick<Attachment, 'size' | 'mime' | 'filePath'>,
capabilities: AttachmentReceiveCapabilities
): boolean {
if (attachment.filePath?.trim()) {
return false;
}
if (!capabilities.canStreamToDisk || !capabilities.canPersistSize(attachment.size)) {
return false;
}
if (attachment.size > MAX_AUTO_SAVE_SIZE_BYTES) {
return true;
}
return isAttachmentMedia(attachment);
}
export function canReceiveAttachmentInMemory(
attachment: Pick<Attachment, 'size'>,
capabilities: AttachmentReceiveCapabilities
): boolean {
if (attachment.size <= MAX_AUTO_SAVE_SIZE_BYTES) {
return true;
}
return !capabilities.canStreamToDisk && capabilities.canPersistSize(attachment.size);
}
export function canReceiveAttachment(
attachment: Pick<Attachment, 'size' | 'mime' | 'filePath'>,
capabilities: AttachmentReceiveCapabilities
): boolean {
return shouldStreamAttachmentReceiveToDisk(attachment, capabilities)
|| canReceiveAttachmentInMemory(attachment, capabilities);
}
function decodeUrlSegment(value: string): string {
try {
return decodeURIComponent(value);