1
Fork 0
mirror of https://github.com/diced/zipline.git synced 2025-04-11 23:31:17 -05:00

fix: ranged requests handled properly

This commit is contained in:
diced 2025-02-03 11:48:41 -08:00
parent 8171a9ac61
commit f408811f60
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
6 changed files with 140 additions and 43 deletions

View file

@ -9,4 +9,5 @@ export abstract class Datasource {
public abstract size(file: string): Promise<number | null>;
public abstract get(file: string): Readable | Promise<Readable>;
public abstract fullSize(): Promise<number>;
public abstract range(file: string, start: number, end: number): Promise<Readable>;
}

View file

@ -11,7 +11,7 @@ export class Local extends Datasource {
}
public async save(file: string, data: Buffer): Promise<void> {
await writeFile(join(this.path, file), data);
await writeFile(join(this.path, file), Uint8Array.from(data));
}
public async delete(file: string): Promise<void> {
@ -56,4 +56,11 @@ export class Local extends Datasource {
return size;
}
public async range(file: string, start: number, end: number): Promise<ReadStream> {
const path = join(this.path, file);
const readStream = createReadStream(path, { start, end });
return readStream;
}
}

View file

@ -81,4 +81,15 @@ export class S3 extends Datasource {
});
});
}
public async range(file: string, start: number, end: number): Promise<Readable> {
return new Promise((res) => {
this.s3.getPartialObject(this.config.bucket, file, start, end, (err, stream) => {
if (err) {
console.log(err);
res(null);
} else res(stream);
});
});
}
}

View file

@ -1,9 +1,20 @@
export function parseRangeHeader(header?: string): [number, number] {
if (!header || !header.startsWith('bytes=')) return [0, Infinity];
export function parseRange(header: string, length: number): [number, number] {
const range = header.trim().substring(6);
const range = header.replace('bytes=', '').split('-');
const start = Number(range[0]) || 0;
const end = Number(range[1]) || Infinity;
let start, end;
if (range.startsWith('-')) {
end = length - 1;
start = length - 1 - Number(range.substring(1));
} else {
const [s, e] = range.split('-').map(Number);
start = s;
end = e || length - 1;
}
if (end > length - 1) {
end = length - 1;
}
return [start, end];
}

View file

@ -2,7 +2,7 @@ import { File } from '@prisma/client';
import { FastifyInstance, FastifyReply } from 'fastify';
import fastifyPlugin from 'fastify-plugin';
import exts from 'lib/exts';
import { parseRangeHeader } from 'lib/utils/range';
import { parseRange } from 'lib/utils/range';
function dbFileDecorator(fastify: FastifyInstance, _, done) {
fastify.decorateReply('dbFile', dbFile);
@ -17,28 +17,70 @@ function dbFileDecorator(fastify: FastifyInstance, _, done) {
const size = await this.server.datasource.size(file.name);
if (size === null) return this.notFound();
// eslint-disable-next-line prefer-const
let [rangeStart, rangeEnd] = parseRangeHeader(this.request.headers.range);
if (rangeStart >= rangeEnd)
return this.code(416)
.header('Content-Range', `bytes 0/${size - 1}`)
.send();
if (rangeEnd === Infinity) rangeEnd = size - 1;
const data = await this.server.datasource.get(file.name);
// only send content-range if the client asked for it
if (this.request.headers.range) {
this.code(206);
this.header('Content-Range', `bytes ${rangeStart}-${rangeEnd}/${size}`);
const [start, end] = parseRange(this.request.headers.range, size);
if (start >= size || end >= size) {
const buf = await datasource.get(file.name);
if (!buf) return this.server.nextServer.render404(this.request.raw, this.raw);
return this.type(file.mimetype || 'application/octet-stream')
.headers({
'Content-Length': size,
...(file.originalName
? {
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${encodeURIComponent(
file.originalName,
)}"`,
}
: download && {
'Content-Disposition': 'attachment;',
}),
})
.status(416)
.send(buf);
}
const buf = await datasource.range(file.name, start || 0, end);
if (!buf) return this.server.nextServer.render404(this.request.raw, this.raw);
return this.type(file.mimetype || 'application/octet-stream')
.headers({
'Content-Range': `bytes ${start}-${end}/${size}`,
'Accept-Ranges': 'bytes',
'Content-Length': end - start + 1,
...(file.originalName
? {
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${encodeURIComponent(
file.originalName,
)}"`,
}
: download && {
'Content-Disposition': 'attachment;',
}),
})
.status(206)
.send(buf);
}
this.header('Content-Length', rangeEnd - rangeStart + 1);
this.header('Content-Type', download ? 'application/octet-stream' : file.mimetype);
this.header('Content-Disposition', `inline; filename="${encodeURI(file.originalName || file.name)}"`);
this.header('Accept-Ranges', 'bytes');
const data = await datasource.get(file.name);
if (!data) return this.server.nextServer.render404(this.request.raw, this.raw);
return this.send(data);
return this.type(file.mimetype || 'application/octet-stream')
.headers({
'Content-Length': size,
'Accept-Ranges': 'bytes',
...(file.originalName
? {
'Content-Disposition': `${download ? 'attachment; ' : ''}filename="${encodeURIComponent(
file.originalName,
)}"`,
}
: download && {
'Content-Disposition': 'attachment;',
}),
})
.status(200)
.send(data);
}
}

View file

@ -5,7 +5,7 @@ import fastifyPlugin from 'fastify-plugin';
import { createBrotliCompress, createDeflate, createGzip } from 'zlib';
import pump from 'pump';
import { Transform } from 'stream';
import { parseRangeHeader } from 'lib/utils/range';
import { parseRange } from 'lib/utils/range';
function rawFileDecorator(fastify: FastifyInstance, _, done) {
fastify.decorateReply('rawFile', rawFile);
@ -18,25 +18,41 @@ function rawFileDecorator(fastify: FastifyInstance, _, done) {
const mimetype = await guess(extname(id).slice(1));
// eslint-disable-next-line prefer-const
let [rangeStart, rangeEnd] = parseRangeHeader(this.request.headers.range);
if (rangeStart >= rangeEnd)
return this.code(416)
.header('Content-Range', `bytes 0/${size - 1}`)
.send();
if (rangeEnd === Infinity) rangeEnd = size - 1;
const data = await this.server.datasource.get(id);
// only send content-range if the client asked for it
if (this.request.headers.range) {
this.code(206);
this.header('Content-Range', `bytes ${rangeStart}-${rangeEnd}/${size}`);
const [start, end] = parseRange(this.request.headers.range, size);
if (start >= size || end >= size) {
const buf = await datasource.get(id);
if (!buf) return this.server.nextServer.render404(this.request.raw, this.raw);
return this.type(mimetype || 'application/octet-stream')
.headers({
'Content-Length': size,
...(download && {
'Content-Disposition': 'attachment;',
}),
})
.status(416)
.send(buf);
}
const buf = await datasource.range(id, start || 0, end);
if (!buf) return this.server.nextServer.render404(this.request.raw, this.raw);
return this.type(mimetype || 'application/octet-stream')
.headers({
'Content-Range': `bytes ${start}-${end}/${size}`,
'Accept-Ranges': 'bytes',
'Content-Length': end - start + 1,
...(download && {
'Content-Disposition': 'attachment;',
}),
})
.status(206)
.send(buf);
}
this.header('Content-Length', rangeEnd - rangeStart + 1);
this.header('Content-Type', download ? 'application/octet-stream' : mimetype);
this.header('Accept-Ranges', 'bytes');
const data = await datasource.get(id);
if (!data) return this.server.nextServer.render404(this.request.raw, this.raw);
if (
this.server.config.core.compression.enabled &&
@ -49,7 +65,16 @@ function rawFileDecorator(fastify: FastifyInstance, _, done) {
)
return this.send(useCompress.call(this, data));
return this.send(data);
return this.type(mimetype || 'application/octet-stream')
.headers({
'Content-Length': size,
'Accept-Ranges': 'bytes',
...(download && {
'Content-Disposition': 'attachment;',
}),
})
.status(200)
.send(data);
}
}