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

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
This commit is contained in:
ari 2024-12-05 23:31:42 +01:00 committed by GitHub
parent 1e507bbf9c
commit c0b2dda7da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 79 additions and 43 deletions

View file

@ -1,8 +1,8 @@
# Use the Prisma binaries image as the first stage
FROM ghcr.io/diced/prisma-binaries:5.1.x as prisma
FROM ghcr.io/diced/prisma-binaries:5.1.x AS prisma
# Use Alpine Linux as the second stage
FROM node:18-alpine3.16 as base
FROM node:18-alpine3.16 AS base
# Set the working directory
WORKDIR /zipline
@ -27,7 +27,7 @@ ENV PRISMA_QUERY_ENGINE_BINARY=/prisma-engines/query-engine \
# Install the dependencies
RUN yarn install --immutable
FROM base as builder
FROM base AS builder
COPY src ./src
COPY next.config.js ./next.config.js

View file

@ -79,7 +79,7 @@
"@types/katex": "^0.16.6",
"@types/minio": "^7.1.1",
"@types/multer": "^1.4.10",
"@types/node": "^18.18.10",
"@types/node": "18",
"@types/qrcode": "^1.5.5",
"@types/react": "^18.2.37",
"@types/sharp": "^0.32.0",

View file

@ -6,7 +6,7 @@ export abstract class Datasource {
public abstract save(file: string, data: Buffer, options?: { type: string }): Promise<void>;
public abstract delete(file: string): Promise<void>;
public abstract clear(): Promise<void>;
public abstract size(file: string): Promise<number>;
public abstract get(file: string): Readable | Promise<Readable>;
public abstract size(file: string): Promise<number | null>;
public abstract get(file: string, start?: number, end?: number): Readable | Promise<Readable>;
public abstract fullSize(): Promise<number>;
}

View file

@ -26,20 +26,20 @@ export class Local extends Datasource {
}
}
public get(file: string): ReadStream {
public get(file: string, start: number = 0, end: number = Infinity): ReadStream {
const full = join(this.path, file);
if (!existsSync(full)) return null;
try {
return createReadStream(full);
return createReadStream(full, { start, end });
} catch (e) {
return null;
}
}
public async size(file: string): Promise<number> {
public async size(file: string): Promise<number | null> {
const full = join(this.path, file);
if (!existsSync(full)) return 0;
if (!existsSync(full)) return null;
const stats = await stat(full);
return stats.size;

View file

@ -1,7 +1,7 @@
import { Datasource } from '.';
import { Readable } from 'stream';
import { ConfigS3Datasource } from 'lib/config/Config';
import { Client } from 'minio';
import { BucketItemStat, Client } from 'minio';
export class S3 extends Datasource {
public name = 'S3';
@ -45,19 +45,34 @@ export class S3 extends Datasource {
});
}
public get(file: string): Promise<Readable> {
public get(file: string, start: number = 0, end: number = Infinity): Promise<Readable> {
return new Promise((res) => {
this.s3.getObject(this.config.bucket, file, (err, stream) => {
if (err) res(null);
else res(stream);
});
this.s3.getPartialObject(
this.config.bucket,
file,
start,
// undefined means to read the rest of the file from the start (offset)
end === Infinity ? undefined : end,
(err, stream) => {
if (err) res(null);
else res(stream);
},
);
});
}
public async size(file: string): Promise<number> {
const stat = await this.s3.statObject(this.config.bucket, file);
return stat.size;
public size(file: string): Promise<number | null> {
return new Promise((res) => {
this.s3.statObject(
this.config.bucket,
file,
// @ts-expect-error this callback is not in the types but the code for it is there
(err: unknown, stat: BucketItemStat) => {
if (err) res(null);
else res(stat.size);
},
);
});
}
public async fullSize(): Promise<number> {

View file

@ -72,12 +72,13 @@ export class Supabase extends Datasource {
}
}
public async get(file: string): Promise<Readable> {
public async get(file: string, start: number = 0, end: number = Infinity): Promise<Readable> {
// get a readable stream from the request
const r = await fetch(`${this.config.url}/storage/v1/object/${this.config.bucket}/${file}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${this.config.key}`,
Range: `bytes=${start}-${end === Infinity ? '' : end}`,
},
});
@ -85,7 +86,7 @@ export class Supabase extends Datasource {
return Readable.fromWeb(r.body as any);
}
public size(file: string): Promise<number> {
public size(file: string): Promise<number | null> {
return new Promise(async (res) => {
fetch(`${this.config.url}/storage/v1/object/list/${this.config.bucket}`, {
method: 'POST',
@ -102,11 +103,11 @@ export class Supabase extends Datasource {
.then((j) => {
if (j.error) {
this.logger.error(`${j.error}: ${j.message}`);
res(0);
res(null);
}
if (j.length === 0) {
res(0);
res(null);
} else {
res(j[0].metadata.size);
}

9
src/lib/utils/range.ts Normal file
View file

@ -0,0 +1,9 @@
export function parseRangeHeader(header?: string): [number, number] {
if (!header || !header.startsWith('bytes=')) return [0, Infinity];
const range = header.replace('bytes=', '').split('-');
const start = Number(range[0]) || 0;
const end = Number(range[1]) || Infinity;
return [start, end];
}

View file

@ -2,6 +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';
function dbFileDecorator(fastify: FastifyInstance, _, done) {
fastify.decorateReply('dbFile', dbFile);
@ -13,19 +14,29 @@ function dbFileDecorator(fastify: FastifyInstance, _, done) {
const ext = file.name.split('.').pop();
if (Object.keys(exts).includes(ext)) return this.server.nextHandle(this.request.raw, this.raw);
const data = await this.server.datasource.get(file.name);
if (!data) return this.notFound();
const size = await this.server.datasource.size(file.name);
if (size === null) return this.notFound();
this.header('Content-Length', size);
// 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 */${size - 1}`)
.send();
if (rangeEnd === Infinity) rangeEnd = size - 1;
const data = await this.server.datasource.get(file.name, rangeStart, rangeEnd);
// 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}`);
}
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)}"`);
if (file.mimetype.startsWith('video/') || file.mimetype.startsWith('audio/')) {
this.header('Accept-Ranges', 'bytes');
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range
this.header('Content-Range', `bytes 0-${size - 1}/${size}`);
}
this.header('Accept-Ranges', 'bytes');
return this.send(data);
}

View file

@ -1956,6 +1956,15 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:18":
version: 18.19.67
resolution: "@types/node@npm:18.19.67"
dependencies:
undici-types: ~5.26.4
checksum: 700f92c6a0b63352ce6327286392adab30bb17623c2a788811e9cf092c4dc2fb5e36ca4727247a981b3f44185fdceef20950a3b7a8ab72721e514ac037022a08
languageName: node
linkType: hard
"@types/node@npm:^10.0.3":
version: 10.17.60
resolution: "@types/node@npm:10.17.60"
@ -1970,15 +1979,6 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:^18.18.10":
version: 18.18.10
resolution: "@types/node@npm:18.18.10"
dependencies:
undici-types: ~5.26.4
checksum: 1245a14a38bfbe115b8af9792dbe87a1c015f2532af5f0a25a073343fefa7b2edfd95ff3830003d1a1278ce7f9ee0e78d4e5454d7a60af65832c8d77f4032ac8
languageName: node
linkType: hard
"@types/normalize-package-data@npm:^2.4.0":
version: 2.4.4
resolution: "@types/normalize-package-data@npm:2.4.4"
@ -11827,7 +11827,7 @@ __metadata:
"@types/katex": ^0.16.6
"@types/minio": ^7.1.1
"@types/multer": ^1.4.10
"@types/node": ^18.18.10
"@types/node": 18
"@types/qrcode": ^1.5.5
"@types/react": ^18.2.37
"@types/sharp": ^0.32.0