Skip to content

Commit c0b2dda

Browse files
authored
feat: proper range request handling (#635)
* fix: update to @types/node@18 this fixes the type error at line 14 of lib/datasources/Local.ts * feat: proper range request handling * fix: docker casing warnings * fix: infinity in header and cleanup * fix: types for s3 and supabase size return value * chore: remove unneeded newline * chore: remove leftover dev comment * fix: don't use 206 & content-range when client did not request it
1 parent 1e507bb commit c0b2dda

File tree

9 files changed

+79
-43
lines changed

9 files changed

+79
-43
lines changed

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Use the Prisma binaries image as the first stage
2-
FROM ghcr.io/diced/prisma-binaries:5.1.x as prisma
2+
FROM ghcr.io/diced/prisma-binaries:5.1.x AS prisma
33

44
# Use Alpine Linux as the second stage
5-
FROM node:18-alpine3.16 as base
5+
FROM node:18-alpine3.16 AS base
66

77
# Set the working directory
88
WORKDIR /zipline
@@ -27,7 +27,7 @@ ENV PRISMA_QUERY_ENGINE_BINARY=/prisma-engines/query-engine \
2727
# Install the dependencies
2828
RUN yarn install --immutable
2929

30-
FROM base as builder
30+
FROM base AS builder
3131

3232
COPY src ./src
3333
COPY next.config.js ./next.config.js

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
"@types/katex": "^0.16.6",
8080
"@types/minio": "^7.1.1",
8181
"@types/multer": "^1.4.10",
82-
"@types/node": "^18.18.10",
82+
"@types/node": "18",
8383
"@types/qrcode": "^1.5.5",
8484
"@types/react": "^18.2.37",
8585
"@types/sharp": "^0.32.0",

src/lib/datasources/Datasource.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export abstract class Datasource {
66
public abstract save(file: string, data: Buffer, options?: { type: string }): Promise<void>;
77
public abstract delete(file: string): Promise<void>;
88
public abstract clear(): Promise<void>;
9-
public abstract size(file: string): Promise<number>;
10-
public abstract get(file: string): Readable | Promise<Readable>;
9+
public abstract size(file: string): Promise<number | null>;
10+
public abstract get(file: string, start?: number, end?: number): Readable | Promise<Readable>;
1111
public abstract fullSize(): Promise<number>;
1212
}

src/lib/datasources/Local.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,20 @@ export class Local extends Datasource {
2626
}
2727
}
2828

29-
public get(file: string): ReadStream {
29+
public get(file: string, start: number = 0, end: number = Infinity): ReadStream {
3030
const full = join(this.path, file);
3131
if (!existsSync(full)) return null;
3232

3333
try {
34-
return createReadStream(full);
34+
return createReadStream(full, { start, end });
3535
} catch (e) {
3636
return null;
3737
}
3838
}
3939

40-
public async size(file: string): Promise<number> {
40+
public async size(file: string): Promise<number | null> {
4141
const full = join(this.path, file);
42-
if (!existsSync(full)) return 0;
42+
if (!existsSync(full)) return null;
4343
const stats = await stat(full);
4444

4545
return stats.size;

src/lib/datasources/S3.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Datasource } from '.';
22
import { Readable } from 'stream';
33
import { ConfigS3Datasource } from 'lib/config/Config';
4-
import { Client } from 'minio';
4+
import { BucketItemStat, Client } from 'minio';
55

66
export class S3 extends Datasource {
77
public name = 'S3';
@@ -45,19 +45,34 @@ export class S3 extends Datasource {
4545
});
4646
}
4747

48-
public get(file: string): Promise<Readable> {
48+
public get(file: string, start: number = 0, end: number = Infinity): Promise<Readable> {
4949
return new Promise((res) => {
50-
this.s3.getObject(this.config.bucket, file, (err, stream) => {
51-
if (err) res(null);
52-
else res(stream);
53-
});
50+
this.s3.getPartialObject(
51+
this.config.bucket,
52+
file,
53+
start,
54+
// undefined means to read the rest of the file from the start (offset)
55+
end === Infinity ? undefined : end,
56+
(err, stream) => {
57+
if (err) res(null);
58+
else res(stream);
59+
},
60+
);
5461
});
5562
}
5663

57-
public async size(file: string): Promise<number> {
58-
const stat = await this.s3.statObject(this.config.bucket, file);
59-
60-
return stat.size;
64+
public size(file: string): Promise<number | null> {
65+
return new Promise((res) => {
66+
this.s3.statObject(
67+
this.config.bucket,
68+
file,
69+
// @ts-expect-error this callback is not in the types but the code for it is there
70+
(err: unknown, stat: BucketItemStat) => {
71+
if (err) res(null);
72+
else res(stat.size);
73+
},
74+
);
75+
});
6176
}
6277

6378
public async fullSize(): Promise<number> {

src/lib/datasources/Supabase.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,20 +72,21 @@ export class Supabase extends Datasource {
7272
}
7373
}
7474

75-
public async get(file: string): Promise<Readable> {
75+
public async get(file: string, start: number = 0, end: number = Infinity): Promise<Readable> {
7676
// get a readable stream from the request
7777
const r = await fetch(`${this.config.url}/storage/v1/object/${this.config.bucket}/${file}`, {
7878
method: 'GET',
7979
headers: {
8080
Authorization: `Bearer ${this.config.key}`,
81+
Range: `bytes=${start}-${end === Infinity ? '' : end}`,
8182
},
8283
});
8384

8485
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8586
return Readable.fromWeb(r.body as any);
8687
}
8788

88-
public size(file: string): Promise<number> {
89+
public size(file: string): Promise<number | null> {
8990
return new Promise(async (res) => {
9091
fetch(`${this.config.url}/storage/v1/object/list/${this.config.bucket}`, {
9192
method: 'POST',
@@ -102,11 +103,11 @@ export class Supabase extends Datasource {
102103
.then((j) => {
103104
if (j.error) {
104105
this.logger.error(`${j.error}: ${j.message}`);
105-
res(0);
106+
res(null);
106107
}
107108

108109
if (j.length === 0) {
109-
res(0);
110+
res(null);
110111
} else {
111112
res(j[0].metadata.size);
112113
}

src/lib/utils/range.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export function parseRangeHeader(header?: string): [number, number] {
2+
if (!header || !header.startsWith('bytes=')) return [0, Infinity];
3+
4+
const range = header.replace('bytes=', '').split('-');
5+
const start = Number(range[0]) || 0;
6+
const end = Number(range[1]) || Infinity;
7+
8+
return [start, end];
9+
}

src/server/decorators/dbFile.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { File } from '@prisma/client';
22
import { FastifyInstance, FastifyReply } from 'fastify';
33
import fastifyPlugin from 'fastify-plugin';
44
import exts from 'lib/exts';
5+
import { parseRangeHeader } from 'lib/utils/range';
56

67
function dbFileDecorator(fastify: FastifyInstance, _, done) {
78
fastify.decorateReply('dbFile', dbFile);
@@ -13,19 +14,29 @@ function dbFileDecorator(fastify: FastifyInstance, _, done) {
1314
const ext = file.name.split('.').pop();
1415
if (Object.keys(exts).includes(ext)) return this.server.nextHandle(this.request.raw, this.raw);
1516

16-
const data = await this.server.datasource.get(file.name);
17-
if (!data) return this.notFound();
18-
1917
const size = await this.server.datasource.size(file.name);
18+
if (size === null) return this.notFound();
19+
20+
// eslint-disable-next-line prefer-const
21+
let [rangeStart, rangeEnd] = parseRangeHeader(this.request.headers.range);
22+
if (rangeStart >= rangeEnd)
23+
return this.code(416)
24+
.header('Content-Range', `bytes */${size - 1}`)
25+
.send();
26+
if (rangeEnd === Infinity) rangeEnd = size - 1;
27+
28+
const data = await this.server.datasource.get(file.name, rangeStart, rangeEnd);
29+
30+
// only send content-range if the client asked for it
31+
if (this.request.headers.range) {
32+
this.code(206);
33+
this.header('Content-Range', `bytes ${rangeStart}-${rangeEnd}/${size}`);
34+
}
2035

21-
this.header('Content-Length', size);
36+
this.header('Content-Length', rangeEnd - rangeStart + 1);
2237
this.header('Content-Type', download ? 'application/octet-stream' : file.mimetype);
2338
this.header('Content-Disposition', `inline; filename="${encodeURI(file.originalName || file.name)}"`);
24-
if (file.mimetype.startsWith('video/') || file.mimetype.startsWith('audio/')) {
25-
this.header('Accept-Ranges', 'bytes');
26-
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range
27-
this.header('Content-Range', `bytes 0-${size - 1}/${size}`);
28-
}
39+
this.header('Accept-Ranges', 'bytes');
2940

3041
return this.send(data);
3142
}

yarn.lock

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1956,6 +1956,15 @@ __metadata:
19561956
languageName: node
19571957
linkType: hard
19581958

1959+
"@types/node@npm:18":
1960+
version: 18.19.67
1961+
resolution: "@types/node@npm:18.19.67"
1962+
dependencies:
1963+
undici-types: ~5.26.4
1964+
checksum: 700f92c6a0b63352ce6327286392adab30bb17623c2a788811e9cf092c4dc2fb5e36ca4727247a981b3f44185fdceef20950a3b7a8ab72721e514ac037022a08
1965+
languageName: node
1966+
linkType: hard
1967+
19591968
"@types/node@npm:^10.0.3":
19601969
version: 10.17.60
19611970
resolution: "@types/node@npm:10.17.60"
@@ -1970,15 +1979,6 @@ __metadata:
19701979
languageName: node
19711980
linkType: hard
19721981

1973-
"@types/node@npm:^18.18.10":
1974-
version: 18.18.10
1975-
resolution: "@types/node@npm:18.18.10"
1976-
dependencies:
1977-
undici-types: ~5.26.4
1978-
checksum: 1245a14a38bfbe115b8af9792dbe87a1c015f2532af5f0a25a073343fefa7b2edfd95ff3830003d1a1278ce7f9ee0e78d4e5454d7a60af65832c8d77f4032ac8
1979-
languageName: node
1980-
linkType: hard
1981-
19821982
"@types/normalize-package-data@npm:^2.4.0":
19831983
version: 2.4.4
19841984
resolution: "@types/normalize-package-data@npm:2.4.4"
@@ -11827,7 +11827,7 @@ __metadata:
1182711827
"@types/katex": ^0.16.6
1182811828
"@types/minio": ^7.1.1
1182911829
"@types/multer": ^1.4.10
11830-
"@types/node": ^18.18.10
11830+
"@types/node": 18
1183111831
"@types/qrcode": ^1.5.5
1183211832
"@types/react": ^18.2.37
1183311833
"@types/sharp": ^0.32.0

0 commit comments

Comments
 (0)