46 lines
3.1 KiB
TypeScript
46 lines
3.1 KiB
TypeScript
import { createReadStream } from 'node:fs';
|
|
import { stat } from 'node:fs/promises';
|
|
import { Readable } from 'node:stream';
|
|
import { json } from '@sveltejs/kit';
|
|
import { getLibraryFile } from './index.js';
|
|
import { assertRegularPath, parseByteRange } from './storage.js';
|
|
|
|
export function gameFileError(cause: unknown): Response {
|
|
const status = typeof cause === 'object' && cause !== null && 'status' in cause && typeof cause.status === 'number' ? cause.status : 400;
|
|
const receivedBytes = typeof cause === 'object' && cause !== null && 'receivedBytes' in cause && Number.isSafeInteger(cause.receivedBytes) ? cause.receivedBytes : undefined;
|
|
return json({ error: cause instanceof Error ? cause.message : 'Game file request failed.', ...(receivedBytes === undefined ? {} : { receivedBytes }) }, { status });
|
|
}
|
|
export async function readGameJson(request: Request, maxBytes = 64 * 1024): Promise<Record<string, unknown>> {
|
|
if (!request.body) throw new Error('A JSON request body is required.');
|
|
const reader = request.body.getReader();
|
|
const chunks: Uint8Array[] = []; let bytes = 0;
|
|
try {
|
|
while (true) {
|
|
const next = await reader.read(); if (next.done) break;
|
|
bytes += next.value.byteLength;
|
|
if (bytes > maxBytes) { await reader.cancel(); throw new Error('Request body exceeds its size limit.'); }
|
|
chunks.push(next.value);
|
|
}
|
|
} finally { reader.releaseLock(); }
|
|
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
if (!body || typeof body !== 'object' || Array.isArray(body)) throw new Error('Expected a JSON object.');
|
|
return body;
|
|
}
|
|
|
|
export async function serveGameFile(request: Request, versionId: string, requestedPath: string): Promise<Response> {
|
|
const file = getLibraryFile(versionId, requestedPath);
|
|
if (!file || file.state !== 'ready') return json({ error: 'File is not in a published manifest.' }, { status: 404 });
|
|
const path = await assertRegularPath(file.storageLocation, file.path);
|
|
if ((await stat(path)).size !== file.size) return json({ error: 'Stored file failed size verification.' }, { status: 503 });
|
|
const etag = `"${file.sha256}"`;
|
|
const headers = new Headers({ 'Content-Type': 'application/octet-stream', 'Accept-Ranges': 'bytes', ETag: etag, 'Cache-Control': 'private, no-cache', 'X-Content-SHA256': file.sha256 });
|
|
if (request.headers.get('if-none-match') === etag) return new Response(null, { status: 304, headers });
|
|
let range;
|
|
try { range = parseByteRange(request.headers.has('if-range') && request.headers.get('if-range') !== etag ? null : request.headers.get('range'), file.size); }
|
|
catch { headers.set('Content-Range', `bytes */${file.size}`); return new Response(null, { status: 416, headers }); }
|
|
headers.set('Content-Length', String(range ? range.end - range.start + 1 : file.size));
|
|
if (range) headers.set('Content-Range', `bytes ${range.start}-${range.end}/${file.size}`);
|
|
const body = request.method === 'HEAD' ? null : Readable.toWeb(createReadStream(path, range ?? undefined)) as ReadableStream<Uint8Array>;
|
|
return new Response(body, { status: range ? 206 : 200, headers });
|
|
}
|