diff --git a/.changeset/lucky-donkeys-listen.md b/.changeset/lucky-donkeys-listen.md new file mode 100644 index 0000000000000..66fb1a4b2b904 --- /dev/null +++ b/.changeset/lucky-donkeys-listen.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes audio attachments not being seekable using the progress slider diff --git a/apps/meteor/server/lib/media/file-upload/config/FileSystem.ts b/apps/meteor/server/lib/media/file-upload/config/FileSystem.ts index a5129b6a3f7e7..eabb89d6062e5 100644 --- a/apps/meteor/server/lib/media/file-upload/config/FileSystem.ts +++ b/apps/meteor/server/lib/media/file-upload/config/FileSystem.ts @@ -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); diff --git a/apps/meteor/server/lib/media/file-upload/config/GridFS.ts b/apps/meteor/server/lib/media/file-upload/config/GridFS.ts index 3b8569c47f6df..933767cf83079 100644 --- a/apps/meteor/server/lib/media/file-upload/config/GridFS.ts +++ b/apps/meteor/server/lib/media/file-upload/config/GridFS.ts @@ -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); diff --git a/apps/meteor/server/lib/media/file-upload/lib/ranges.spec.ts b/apps/meteor/server/lib/media/file-upload/lib/ranges.spec.ts new file mode 100644 index 0000000000000..26c1f1d84406d --- /dev/null +++ b/apps/meteor/server/lib/media/file-upload/lib/ranges.spec.ts @@ -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 }); + }); + + 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 }); + }); +}); diff --git a/apps/meteor/server/lib/media/file-upload/lib/ranges.ts b/apps/meteor/server/lib/media/file-upload/lib/ranges.ts index 0fedb0e981fac..725cec374c846 100644 --- a/apps/meteor/server/lib/media/file-upload/lib/ranges.ts +++ b/apps/meteor/server/lib/media/file-upload/lib/ranges.ts @@ -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