Depot API & v4 (#298)

* feat: nginx + torrential basics & services system

* fix: lint + i18n

* fix: update torrential to remove openssl

* feat: add torrential to Docker build

* feat: move to self hosted runner

* fix: move off self-hosted runner

* fix: update nginx.conf

* feat: torrential cache invalidation

* fix: update torrential for cache invalidation

* feat: integrity check task

* fix: lint

* feat: move to version ids

* fix: client fixes and client-side checks

* feat: new depot apis and version id fixes

* feat: update torrential

* feat: droplet bump and remove unsafe update functions

* fix: lint

* feat: v4 featureset: emulators, multi-launch commands

* fix: lint

* fix: mobile ui for game editor

* feat: launch options

* fix: lint

* fix: remove axios, use $fetch

* feat: metadata and task api improvements

* feat: task actions

* fix: slight styling issue

* feat: fix style and lints

* feat: totp backend routes

* feat: oidc groups

* fix: update drop-base

* feat: creation of passkeys & totp

* feat: totp signin

* feat: webauthn mfa/signin

* feat: launch selecting ui

* fix: manually running tasks

* feat: update add company game modal to use new SelectorGame

* feat: executor selector

* fix(docker): update rust to rust nightly for torrential build (#305)

* feat: new version ui

* feat: move package lookup to build time to allow for deno dev

* fix: lint

* feat: localisation cleanup

* feat: apply localisation cleanup

* feat: potential i18n refactor logic

* feat: remove args from commands

* fix: lint

* fix: lockfile

---------

Co-authored-by: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
This commit is contained in:
DecDuck
2026-01-13 15:32:39 +11:00
committed by GitHub
parent 8ef983304c
commit 63ac2b8ffc
190 changed files with 5848 additions and 2309 deletions
+67 -72
View File
@@ -9,7 +9,6 @@ import path from "path";
import prisma from "../db/database";
import { fuzzy } from "fast-fuzzy";
import taskHandler from "../tasks";
import { parsePlatform } from "../utils/parseplatform";
import notificationSystem from "../notifications";
import { GameNotFoundError, type LibraryProvider } from "./provider";
import { logger } from "../logging";
@@ -17,6 +16,8 @@ import type { GameModel } from "~/prisma/client/models";
import { createHash } from "node:crypto";
import type { WorkingLibrarySource } from "~/server/api/v1/admin/library/sources/index.get";
import gameSizeManager from "~/server/internal/gamesize";
import { TORRENTIAL_SERVICE } from "../services/services/torrential";
import type { ImportVersion } from "~/server/api/v1/admin/import/version/index.post";
export function createGameImportTaskId(libraryId: string, libraryPath: string) {
return createHash("md5")
@@ -24,7 +25,10 @@ export function createGameImportTaskId(libraryId: string, libraryPath: string) {
.digest("hex");
}
export function createVersionImportTaskId(gameId: string, versionName: string) {
export function createVersionImportTaskKey(
gameId: string,
versionName: string,
) {
return createHash("md5")
.update(`import:${gameId}:${versionName}`)
.digest("hex");
@@ -41,6 +45,10 @@ class LibraryManager {
this.libraries.delete(id);
}
getLibrary(libraryId: string): LibraryProvider<unknown> | undefined {
return this.libraries.get(libraryId);
}
async fetchLibraries(): Promise<WorkingLibrarySource[]> {
const libraries = await prisma.library.findMany({});
@@ -79,7 +87,7 @@ class LibraryManager {
const providerUnimportedGames = providerGames.filter(
(libraryPath) =>
!instanceGames[id]?.[libraryPath] &&
!taskHandler.hasTask(createGameImportTaskId(id, libraryPath)),
!taskHandler.hasTaskKey(createGameImportTaskId(id, libraryPath)),
);
unimportedGames[id] = providerUnimportedGames;
}
@@ -107,12 +115,12 @@ class LibraryManager {
try {
const versions = await provider.listVersions(
libraryPath,
game.versions.map((v) => v.versionName),
game.versions.map((v) => v.versionPath),
);
const unimportedVersions = versions.filter(
(e) =>
game.versions.findIndex((v) => v.versionName == e) == -1 &&
!taskHandler.hasTask(createVersionImportTaskId(game.id, e)),
game.versions.findIndex((v) => v.versionPath == e) == -1 &&
!taskHandler.hasTaskKey(createVersionImportTaskKey(game.id, e)),
);
return unimportedVersions;
} catch (e) {
@@ -127,12 +135,8 @@ class LibraryManager {
async fetchGamesWithStatus() {
const games = await prisma.game.findMany({
include: {
versions: {
select: {
versionName: true,
},
},
library: true,
versions: true,
},
orderBy: {
mName: "asc",
@@ -209,7 +213,7 @@ class LibraryManager {
if (checkExt != ext) continue;
const fuzzyValue = fuzzy(basename, game.mName);
options.push({
filename,
filename: filename.replaceAll(" ", "\\ "),
platform,
match: fuzzyValue,
});
@@ -243,24 +247,10 @@ class LibraryManager {
async importVersion(
gameId: string,
versionName: string,
metadata: {
platform: string;
onlySetup: boolean;
setup: string;
setupArgs: string;
launch: string;
launchArgs: string;
delta: boolean;
umuId: string;
},
versionPath: string,
metadata: typeof ImportVersion.infer,
) {
const taskId = createVersionImportTaskId(gameId, versionName);
const platform = parsePlatform(metadata.platform);
if (!platform) return undefined;
const taskKey = createVersionImportTaskKey(gameId, versionPath);
const game = await prisma.game.findUnique({
where: { id: gameId },
@@ -271,17 +261,17 @@ class LibraryManager {
const library = this.libraries.get(game.libraryId);
if (!library) return undefined;
taskHandler.create({
id: taskId,
return await taskHandler.create({
key: taskKey,
taskGroup: "import:game",
name: `Importing version ${versionName} for ${game.mName}`,
name: `Importing version ${versionPath} for ${game.mName}`,
acls: ["system:import:version:read"],
async run({ progress, logger }) {
// First, create the manifest via droplet.
// This takes up 90% of our progress, so we wrap it in a *0.9
const manifest = await library.generateDropletManifest(
game.libraryPath,
versionName,
versionPath,
(err, value) => {
if (err) throw err;
progress(value * 0.9);
@@ -299,59 +289,64 @@ class LibraryManager {
});
// Then, create the database object
if (metadata.onlySetup) {
await prisma.gameVersion.create({
data: {
gameId: gameId,
versionName: versionName,
dropletManifest: manifest,
versionIndex: currentIndex,
delta: metadata.delta,
umuIdOverride: metadata.umuId,
platform: platform,
onlySetup: true,
setupCommand: metadata.setup,
setupArgs: metadata.setupArgs.split(" "),
await prisma.gameVersion.create({
data: {
game: {
connect: {
id: gameId,
},
},
});
} else {
await prisma.gameVersion.create({
data: {
gameId: gameId,
versionName: versionName,
dropletManifest: manifest,
versionIndex: currentIndex,
delta: metadata.delta,
umuIdOverride: metadata.umuId,
platform: platform,
onlySetup: false,
setupCommand: metadata.setup,
setupArgs: metadata.setupArgs.split(" "),
launchCommand: metadata.launch,
launchArgs: metadata.launchArgs.split(" "),
displayName: metadata.displayName ?? null,
versionPath,
dropletManifest: manifest,
versionIndex: currentIndex,
delta: metadata.delta,
onlySetup: metadata.onlySetup,
setups: {
createMany: {
data: metadata.setups.map((v) => ({
command: v.launch,
platform: v.platform,
})),
},
},
});
}
launches: {
createMany: !metadata.onlySetup
? {
data: metadata.launches.map((v) => ({
name: v.name,
command: v.launch,
platform: v.platform,
...(v.executorId
? { executorId: v.executorId }
: undefined),
})),
}
: { data: [] },
},
},
});
logger.info("Successfully created version!");
notificationSystem.systemPush({
nonce: `version-create-${gameId}-${versionName}`,
title: `'${game.mName}' ('${versionName}') finished importing.`,
description: `Drop finished importing version ${versionName} for ${game.mName}.`,
nonce: `version-create-${gameId}-${versionPath}`,
title: `'${game.mName}' ('${versionPath}') finished importing.`,
description: `Drop finished importing version ${versionPath} for ${game.mName}.`,
actions: [`View|/admin/library/${gameId}`],
acls: ["system:import:version:read"],
});
await libraryManager.cacheCombinedGameSize(gameId);
await libraryManager.cacheGameVersionSize(gameId, versionName);
await libraryManager.cacheGameVersionSize(gameId, versionPath);
await TORRENTIAL_SERVICE.utils().invalidate(gameId, versionPath);
progress(100);
},
});
return taskId;
}
async peekFile(
@@ -381,7 +376,7 @@ class LibraryManager {
await prisma.gameVersion.deleteMany({
where: {
gameId: gameId,
versionName: version,
versionId: version,
},
});