Skip to content
Merged
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/lucky-donkeys-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Fixes audio attachments not being seekable using the progress slider
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const FileSystemUploads = new FileUploadClass({
res.setHeader('Content-Disposition', `${getContentDisposition(req)}; filename*=UTF-8''${encodeURIComponent(file.name || '')}`);
file.uploadedAt && res.setHeader('Last-Modified', file.uploadedAt.toUTCString());
res.setHeader('Content-Type', file.type || 'application/octet-stream');
res.setHeader('Accept-Ranges', 'bytes');

if (req.headers.range) {
const range = getFileRange(file, req);
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/server/lib/media/file-upload/config/GridFS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ const readFromGridFS = async function (
const rs = await store.getReadStream(fileId, file);
const ws = new stream.PassThrough();

res.setHeader('Accept-Ranges', 'bytes');

[rs, ws].forEach((stream) =>
stream.on('error', (err) => {
store.onReadError.call(store, err, fileId, file);
Expand Down
63 changes: 63 additions & 0 deletions apps/meteor/server/lib/media/file-upload/lib/ranges.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type http from 'node:http';

import type { IUpload } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { describe, it } from 'mocha';

import { getFileRange } from './ranges';

const req = (range?: string) => ({ headers: { range } }) as http.IncomingMessage;
const file = (size: number) => ({ size }) as IUpload;

describe('getFileRange', () => {
it('returns undefined when no Range header is present', () => {
expect(getFileRange(file(1000), req())).to.equal(undefined);
});

it('returns undefined for a malformed Range header', () => {
expect(getFileRange(file(1000), req('bytes=abc'))).to.equal(undefined);
expect(getFileRange(file(1000), req('items=0-99'))).to.equal(undefined);
expect(getFileRange(file(1000), req('bytes=-'))).to.equal(undefined);
});

it('returns undefined when the file has no size', () => {
expect(getFileRange(file(0), req('bytes=0-'))).to.equal(undefined);
});

it('serves an open-ended range `bytes=0-` as the whole file', () => {
expect(getFileRange(file(1000), req('bytes=0-'))).to.deep.equal({ outOfRange: false, start: 0, stop: 999 });
});

it('serves an open-ended range from a mid-file offset to EOF', () => {
expect(getFileRange(file(1000), req('bytes=77-'))).to.deep.equal({ outOfRange: false, start: 77, stop: 999 });
});

it('serves an explicit closed range', () => {
expect(getFileRange(file(1000), req('bytes=100-199'))).to.deep.equal({ outOfRange: false, start: 100, stop: 199 });
});

it('clamps an end that runs past EOF', () => {
expect(getFileRange(file(1000), req('bytes=100-5000'))).to.deep.equal({ outOfRange: false, start: 100, stop: 999 });
});

it('serves a suffix range `bytes=-N` as the last N bytes', () => {
expect(getFileRange(file(1000), req('bytes=-200'))).to.deep.equal({ outOfRange: false, start: 800, stop: 999 });
});

it('clamps a suffix range larger than the file to the whole file', () => {
expect(getFileRange(file(1000), req('bytes=-5000'))).to.deep.equal({ outOfRange: false, start: 0, stop: 999 });
});

it('flags a start at or beyond EOF as out of range', () => {
expect(getFileRange(file(1000), req('bytes=1000-'))).to.deep.equal({ outOfRange: true, start: 1000, stop: 999 });
expect(getFileRange(file(1000), req('bytes=1500-1600'))).to.deep.equal({ outOfRange: true, start: 1500, stop: 999 });
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it('flags an inverted closed range as out of range', () => {
expect(getFileRange(file(1000), req('bytes=200-100'))).to.deep.equal({ outOfRange: true, start: 200, stop: 100 });
});

it('serves the final byte of the file', () => {
expect(getFileRange(file(1000), req('bytes=999-'))).to.deep.equal({ outOfRange: false, start: 999, stop: 999 });
});
});
41 changes: 32 additions & 9 deletions apps/meteor/server/lib/media/file-upload/lib/ranges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,54 @@ import type http from 'node:http';

import type { IUpload } from '@rocket.chat/core-typings';

function getByteRange(header?: string) {
type ByteRange = { start?: number; stop?: number };

function parseByteRange(header?: string): ByteRange | undefined {
if (!header) {
return;
}
const matches = header.match(/(\d+)-(\d+)/);
const matches = header.match(/^bytes=(\d*)-(\d*)$/);
if (!matches) {
return;
}
const [, startStr, stopStr] = matches;
if (startStr === '' && stopStr === '') {
return;
}
return {
start: parseInt(matches[1], 10),
stop: parseInt(matches[2], 10),
start: startStr === '' ? undefined : parseInt(startStr, 10),
stop: stopStr === '' ? undefined : parseInt(stopStr, 10),
};
}

export function getFileRange(file: IUpload, req: http.IncomingMessage) {
const range = getByteRange(req.headers.range);
if (!range) {
const parsed = parseByteRange(req.headers.range);
if (!parsed) {
return;
}

const size = file.size || 0;
if (range.start > size || range.stop <= range.start || range.stop > size) {
return { outOfRange: true, start: range.start, stop: range.stop };
if (size <= 0) {
return;
}

let { start, stop } = parsed;

if (start === undefined) {
if (!stop) {
return { outOfRange: true, start: 0, stop: size - 1 };
}
start = Math.max(0, size - stop);
stop = size - 1;
} else if (stop === undefined || stop > size - 1) {
stop = size - 1;
}

if (start > stop || start >= size) {
return { outOfRange: true, start, stop };
}

return { outOfRange: false, start: range.start, stop: range.stop };
return { outOfRange: false, start, stop };
}

// code from: https://github.com/jalik/jalik-ufs/blob/master/ufs-server.js#L310
Expand Down
Loading