From 3dd6062af4c737c1598be0c4e0c86d0b93eb607d Mon Sep 17 00:00:00 2001 From: DecDuck Date: Wed, 23 Oct 2024 12:03:31 +1100 Subject: [PATCH] added download chunk endpoint --- server/api/v1/client/chunk.get.ts | 51 ++++++++++++++++++++++++ server/internal/clients/event-handler.ts | 9 +++-- server/internal/library/index.ts | 24 ++++++----- 3 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 server/api/v1/client/chunk.get.ts diff --git a/server/api/v1/client/chunk.get.ts b/server/api/v1/client/chunk.get.ts new file mode 100644 index 0000000..1678dc8 --- /dev/null +++ b/server/api/v1/client/chunk.get.ts @@ -0,0 +1,51 @@ +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import prisma from "~/server/internal/db/database"; +import fs from "fs"; +import path from "path"; +import libraryManager from "~/server/internal/library"; + +const chunkSize = 1024 * 1024 * 64; + +export default defineClientEventHandler(async (h3) => { + const query = getQuery(h3); + const gameId = query.id?.toString(); + const versionName = query.versionName?.toString(); + const filename = query.name?.toString(); + const chunkIndex = parseInt(query.chunk?.toString() ?? "?"); + + if (!gameId || !versionName || !filename || !Number.isNaN(chunkIndex)) + throw createError({ + statusCode: 400, + statusMessage: "Invalid chunk arguments", + }); + + const game = await prisma.game.findUnique({ + where: { + id: gameId, + }, + select: { + libraryBasePath: true, + }, + }); + if (!game) + throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); + + const versionDir = path.join(libraryManager.fetchLibraryPath(), versionName); + if (!fs.existsSync(versionDir)) + throw createError({ + statusCode: 400, + statusMessage: "Invalid version name", + }); + + const gameFile = path.join(versionDir, filename); + if (!fs.existsSync(versionDir)) + throw createError({ statusCode: 400, statusMessage: "Invalid game file" }); + + const gameFileStats = fs.statSync(gameFile); + + const start = chunkIndex * chunkSize; + const end = Math.min((chunkIndex + 1) * chunkSize, gameFileStats.size); + const gameReadStream = fs.createReadStream(gameFile, { start, end }); + + return sendStream(h3, gameReadStream); +}); diff --git a/server/internal/clients/event-handler.ts b/server/internal/clients/event-handler.ts index 556d45f..f5f39dc 100644 --- a/server/internal/clients/event-handler.ts +++ b/server/internal/clients/event-handler.ts @@ -5,7 +5,7 @@ import prisma from "../db/database"; export type EventHandlerFunction = ( h3: H3Event, - utils: ClientUtils + utils: ClientUtils, ) => Promise | T; type ClientUtils = { @@ -18,7 +18,7 @@ const NONCE_LENIENCE = 30_000; export function defineClientEventHandler(handler: EventHandlerFunction) { return defineEventHandler(async (h3) => { - const header = await getHeader(h3, "Authorization"); + const header = getHeader(h3, "Authorization"); if (!header) throw createError({ statusCode: 403 }); const [method, ...parts] = header.split(" "); @@ -49,6 +49,7 @@ export function defineClientEventHandler(handler: EventHandlerFunction) { const ca = h3.context.ca; const certBundle = await ca.fetchClientCertificate(clientId); + // This does the blacklist check already if (!certBundle) throw createError({ statusCode: 403, @@ -80,7 +81,7 @@ export function defineClientEventHandler(handler: EventHandlerFunction) { }); if (!client) throw new Error( - "client util fetch client broke - this should NOT happen" + "client util fetch client broke - this should NOT happen", ); return client; } @@ -95,7 +96,7 @@ export function defineClientEventHandler(handler: EventHandlerFunction) { if (!client) throw new Error( - "client util fetch client broke - this should NOT happen" + "client util fetch client broke - this should NOT happen", ); return client.user; diff --git a/server/internal/library/index.ts b/server/internal/library/index.ts index 8f1878b..9d2e717 100644 --- a/server/internal/library/index.ts +++ b/server/internal/library/index.ts @@ -22,6 +22,10 @@ class LibraryManager { this.basePath = process.env.LIBRARY ?? "./.data/library"; } + fetchLibraryPath() { + return this.basePath; + } + async fetchAllUnimportedGames() { const dirs = fs.readdirSync(this.basePath).filter((e) => { const fullDir = path.join(this.basePath, e); @@ -45,13 +49,13 @@ class LibraryManager { async fetchUnimportedGameVersions( libraryBasePath: string, - versions: Array + versions: Array, ) { const gameDir = path.join(this.basePath, libraryBasePath); const versionsDirs = fs.readdirSync(gameDir); const importedVersionDirs = versions.map((e) => e.versionName); const unimportedVersions = versionsDirs.filter( - (e) => !importedVersionDirs.includes(e) + (e) => !importedVersionDirs.includes(e), ); return unimportedVersions; @@ -79,10 +83,10 @@ class LibraryManager { noVersions: e.versions.length == 0, unimportedVersions: await this.fetchUnimportedGameVersions( e.libraryBasePath, - e.versions + e.versions, ), }, - })) + })), ); } @@ -103,13 +107,13 @@ class LibraryManager { const targetDir = path.join(this.basePath, game.libraryBasePath); if (!fs.existsSync(targetDir)) throw new Error( - "Game in database, but no physical directory? Something is very very wrong..." + "Game in database, but no physical directory? Something is very very wrong...", ); const versions = fs.readdirSync(targetDir); const currentVersions = game.versions.map((e) => e.versionName); const unimportedVersions = versions.filter( - (e) => !currentVersions.includes(e) + (e) => !currentVersions.includes(e), ); return unimportedVersions; } @@ -123,7 +127,7 @@ class LibraryManager { const targetDir = path.join( this.basePath, game.libraryBasePath, - versionName + versionName, ); if (!fs.existsSync(targetDir)) return undefined; @@ -171,7 +175,7 @@ class LibraryManager { const finalChoice = sortedOptions[0]; const finalChoiceRelativePath = path.relative( targetDir, - finalChoice.filename + finalChoice.filename, ); startupGuess = finalChoiceRelativePath; platformGuess = finalChoice.platform; @@ -196,7 +200,7 @@ class LibraryManager { gameId: string, versionName: string, metadata: { platform: string; setup: string; startup: string }, - delta = false + delta = false, ) { const taskId = `import:${gameId}:${versionName}`; @@ -233,7 +237,7 @@ class LibraryManager { (err, manifest) => { if (err) return reject(err); resolve(manifest); - } + }, ); });