@vida-global/core 2.4.2 → 2.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -248,13 +248,53 @@ const InstanceMethods = {
|
|
|
248
248
|
if (this.rendered) return;
|
|
249
249
|
if (this.isStreaming) return;
|
|
250
250
|
|
|
251
|
+
this._response.setHeader('Accept-Ranges', 'bytes');
|
|
252
|
+
const range = Buffer.isBuffer(body) ? this._requestedByteRange(body.length) : null;
|
|
253
|
+
if (range?.unsatisfiable) {
|
|
254
|
+
this._response.status(416);
|
|
255
|
+
this._response.setHeader('Content-Range', `bytes */${body.length}`);
|
|
256
|
+
this._send('');
|
|
257
|
+
this.markRendered();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
251
261
|
this._response.setHeader('Content-Type', contentType);
|
|
252
262
|
if (filename) this._response.attachment(filename);
|
|
263
|
+
if (range) {
|
|
264
|
+
this._response.status(206);
|
|
265
|
+
this._response.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${body.length}`);
|
|
266
|
+
body = body.subarray(range.start, range.end + 1);
|
|
267
|
+
}
|
|
253
268
|
this._send(body);
|
|
254
269
|
this.markRendered();
|
|
255
270
|
},
|
|
256
271
|
|
|
257
272
|
|
|
273
|
+
// Single-range only: a multi-range request gets the whole body, which media players handle
|
|
274
|
+
// better than a multipart/byteranges reply.
|
|
275
|
+
_requestedByteRange(totalBytes) {
|
|
276
|
+
const header = this._request.headers?.range;
|
|
277
|
+
if (typeof header !== 'string') return null;
|
|
278
|
+
|
|
279
|
+
const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
|
|
280
|
+
if (!match || (!match[1] && !match[2])) return null;
|
|
281
|
+
|
|
282
|
+
let start, end;
|
|
283
|
+
if (!match[1]) {
|
|
284
|
+
const suffix = Number(match[2]);
|
|
285
|
+
if (suffix === 0) return { unsatisfiable: true };
|
|
286
|
+
start = Math.max(0, totalBytes - suffix);
|
|
287
|
+
end = totalBytes - 1;
|
|
288
|
+
} else {
|
|
289
|
+
start = Number(match[1]);
|
|
290
|
+
end = match[2] ? Math.min(Number(match[2]), totalBytes - 1) : totalBytes - 1;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (start >= totalBytes || start > end) return { unsatisfiable: true };
|
|
294
|
+
return { start, end };
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
|
|
258
298
|
renderFile(filePath, { contentType, cacheControl, maxAge } = {}) {
|
|
259
299
|
if (!filePath) throw new Error('renderFile requires a filePath');
|
|
260
300
|
const resolvedType = contentType || guessContentType(filePath);
|
package/package.json
CHANGED
|
@@ -758,11 +758,12 @@ describe('VidaServerController', () => {
|
|
|
758
758
|
|
|
759
759
|
|
|
760
760
|
beforeEach(() => {
|
|
761
|
-
request = {};
|
|
761
|
+
request = { headers: {} };
|
|
762
762
|
response = {
|
|
763
763
|
setHeader: jest.fn(),
|
|
764
764
|
attachment: jest.fn(),
|
|
765
765
|
send: jest.fn(),
|
|
766
|
+
status: jest.fn(),
|
|
766
767
|
};
|
|
767
768
|
controller = new FooController(request, response);
|
|
768
769
|
});
|
|
@@ -774,6 +775,52 @@ describe('VidaServerController', () => {
|
|
|
774
775
|
controller.renderBinary(body, { contentType: 'audio/wav' });
|
|
775
776
|
|
|
776
777
|
expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'audio/wav');
|
|
778
|
+
expect(response.setHeader).toHaveBeenCalledWith('Accept-Ranges', 'bytes');
|
|
779
|
+
expect(response.status).not.toHaveBeenCalled();
|
|
780
|
+
expect(response.send).toHaveBeenCalledWith(body);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
it.each([
|
|
785
|
+
['a bounded range', 'bytes=2-5', 'bytes 2-5/10', '2345'],
|
|
786
|
+
['an open-ended range', 'bytes=7-', 'bytes 7-9/10', '789'],
|
|
787
|
+
['a suffix range', 'bytes=-3', 'bytes 7-9/10', '789'],
|
|
788
|
+
['an overshooting end', 'bytes=8-99', 'bytes 8-9/10', '89'],
|
|
789
|
+
])('serves %s as 206 Partial Content', (_label, range, contentRange, slice) => {
|
|
790
|
+
request.headers.range = range;
|
|
791
|
+
|
|
792
|
+
controller.renderBinary(Buffer.from('0123456789'), { contentType: 'audio/wav' });
|
|
793
|
+
|
|
794
|
+
expect(response.status).toHaveBeenCalledWith(206);
|
|
795
|
+
expect(response.setHeader).toHaveBeenCalledWith('Content-Range', contentRange);
|
|
796
|
+
expect(response.send).toHaveBeenCalledWith(Buffer.from(slice));
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
it ('answers an unsatisfiable range with 416 and the total size', () => {
|
|
801
|
+
request.headers.range = 'bytes=10-';
|
|
802
|
+
|
|
803
|
+
controller.renderBinary(Buffer.from('0123456789'), { contentType: 'audio/wav' });
|
|
804
|
+
|
|
805
|
+
expect(response.status).toHaveBeenCalledWith(416);
|
|
806
|
+
expect(response.setHeader).toHaveBeenCalledWith('Content-Range', 'bytes */10');
|
|
807
|
+
expect(response.send).toHaveBeenCalledWith('');
|
|
808
|
+
expect(controller.rendered).toBe(true);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
it.each([
|
|
813
|
+
['a multi-range request', 'bytes=0-1,5-9'],
|
|
814
|
+
['an empty range', 'bytes=-'],
|
|
815
|
+
['a non-byte unit', 'items=0-5'],
|
|
816
|
+
['garbage', 'garbage'],
|
|
817
|
+
])('serves the whole body for %s', (_label, range) => {
|
|
818
|
+
request.headers.range = range;
|
|
819
|
+
const body = Buffer.from('0123456789');
|
|
820
|
+
|
|
821
|
+
controller.renderBinary(body, { contentType: 'audio/wav' });
|
|
822
|
+
|
|
823
|
+
expect(response.status).not.toHaveBeenCalled();
|
|
777
824
|
expect(response.send).toHaveBeenCalledWith(body);
|
|
778
825
|
});
|
|
779
826
|
|