import { createReadStream } from 'node:fs'; import { stat } from 'node:fs/promises'; import { Readable } from 'node:stream'; import { json } from '@sveltejs/kit'; import { getLibraryFile, libraryFilesRetired } 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> { 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 { if (libraryFilesRetired(versionId)) return json({ error: 'These outdated game files have been removed.' }, { status: 410 }); 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; return new Response(body, { status: range ? 206 : 200, headers }); }