Files
electric-cherry/src/lib/server/game-files/http.ts
T

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 });
}