mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-18 10:41:11 +10:00
Rearchitecture for v0.4.0 (#197)
* feat: database redist support * feat: rearchitecture of database schemas, migration reset, and #180 * feat: import redists * fix: giantbomb logging bug * feat: partial user platform support + statusMessage -> message * feat: add user platform filters to store view * fix: sanitize svg uploads ... copilot suggested this I feel dirty. * feat: beginnings of platform & redist management * feat: add server side redist patching * fix: update drop-base commit * feat: import of custom platforms & file extensions * fix: redelete platform * fix: remove platform * feat: uninstall commands, new R UI * checkpoint: before migrating to nuxt v4 * update to nuxt 4 * fix: fixes for Nuxt v4 update * fix: remaining type issues * feat: initial feedback to import other kinds of versions * working commit * fix: lint * feat: redist import
This commit is contained in:
@ -73,6 +73,10 @@ export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
||||
"game:image:new": "Upload an image for a game.",
|
||||
"game:image:delete": "Delete an image for a game.",
|
||||
|
||||
"redist:read": "Fetch redistributables on this instance.",
|
||||
"redist:update": "Update redistributables on this instance.",
|
||||
"redist:delete": "Delete redistributables on this instance.",
|
||||
|
||||
"company:read": "Fetch companies.",
|
||||
"company:create": "Create a new company.",
|
||||
"company:update": "Update existing companies.",
|
||||
@ -84,6 +88,8 @@ export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
||||
"import:game:read":
|
||||
"Fetch games to be imported, and search the metadata for games.",
|
||||
"import:game:new": "Import a game.",
|
||||
"import:redist:read": "Fetch redists to be imported.",
|
||||
"import:redist:new": "Import a redist.",
|
||||
|
||||
"tags:read": "Fetch all tags",
|
||||
"tags:create": "Create a tag",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { APITokenMode } from "~/prisma/client/enums";
|
||||
import { APITokenMode } from "~~/prisma/client/enums";
|
||||
import prisma from "../db/database";
|
||||
import sessionHandler from "../session";
|
||||
import type { MinimumRequestObject } from "~/server/h3";
|
||||
import type { MinimumRequestObject } from "~~/server/h3";
|
||||
|
||||
export const userACLs = [
|
||||
"read",
|
||||
@ -67,6 +67,10 @@ export const systemACLs = [
|
||||
"game:image:new",
|
||||
"game:image:delete",
|
||||
|
||||
"redist:read",
|
||||
"redist:update",
|
||||
"redist:delete",
|
||||
|
||||
"company:read",
|
||||
"company:update",
|
||||
"company:create",
|
||||
@ -74,9 +78,10 @@ export const systemACLs = [
|
||||
|
||||
"import:version:read",
|
||||
"import:version:new",
|
||||
|
||||
"import:game:read",
|
||||
"import:game:new",
|
||||
"import:redist:read",
|
||||
"import:redist:new",
|
||||
|
||||
"user:read",
|
||||
"user:delete",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { AuthMec } from "~/prisma/client/enums";
|
||||
import { AuthMec } from "~~/prisma/client/enums";
|
||||
import { OIDCManager } from "./oidc";
|
||||
import { logger } from "~/server/internal/logging";
|
||||
import { logger } from "~~/server/internal/logging";
|
||||
|
||||
class AuthManager {
|
||||
private authProviders: {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import prisma from "../../db/database";
|
||||
import type { UserModel } from "~/prisma/client/models";
|
||||
import { AuthMec } from "~/prisma/client/enums";
|
||||
import type { UserModel } from "~~/prisma/client/models";
|
||||
import { AuthMec } from "~~/prisma/client/enums";
|
||||
import objectHandler from "../../objects";
|
||||
import type { Readable } from "stream";
|
||||
import * as jdenticon from "jdenticon";
|
||||
import { systemConfig } from "../../config/sys-conf";
|
||||
import { logger } from "~/server/internal/logging";
|
||||
import { logger } from "~~/server/internal/logging";
|
||||
|
||||
interface OIDCWellKnown {
|
||||
authorization_endpoint: string;
|
||||
|
||||
@ -24,7 +24,7 @@ export class CertificateAuthority {
|
||||
let ca;
|
||||
if (root === undefined) {
|
||||
const [cert, priv] = droplet.generateRootCa();
|
||||
const bundle: CertificateBundle = { priv, cert };
|
||||
const bundle: CertificateBundle = { priv: priv!, cert: cert! };
|
||||
await store.store("ca", bundle);
|
||||
ca = new CertificateAuthority(store, bundle);
|
||||
} else {
|
||||
@ -50,8 +50,8 @@ export class CertificateAuthority {
|
||||
caCertificate.priv,
|
||||
);
|
||||
const certBundle: CertificateBundle = {
|
||||
priv,
|
||||
cert,
|
||||
priv: priv!,
|
||||
cert: cert!,
|
||||
};
|
||||
return certBundle;
|
||||
}
|
||||
|
||||
@ -1,29 +1,19 @@
|
||||
import type { EnumDictionary } from "../utils/types";
|
||||
import prisma from "../db/database";
|
||||
import { ClientCapabilities } from "~/prisma/client/enums";
|
||||
import { ClientCapabilities } from "~~/prisma/client/enums";
|
||||
|
||||
// These values are technically mapped to the database,
|
||||
// but Typescript/Prisma doesn't let me link them
|
||||
// They are also what are required by clients in the API
|
||||
// BREAKING CHANGE
|
||||
export enum InternalClientCapability {
|
||||
PeerAPI = "peerAPI",
|
||||
UserStatus = "userStatus",
|
||||
CloudSaves = "cloudSaves",
|
||||
TrackPlaytime = "trackPlaytime",
|
||||
}
|
||||
|
||||
export const validCapabilities = Object.values(InternalClientCapability);
|
||||
export const validCapabilities = Object.values(ClientCapabilities);
|
||||
|
||||
export type CapabilityConfiguration = {
|
||||
[InternalClientCapability.PeerAPI]: object;
|
||||
[InternalClientCapability.UserStatus]: object;
|
||||
[InternalClientCapability.CloudSaves]: object;
|
||||
[ClientCapabilities.PeerAPI]: object;
|
||||
[ClientCapabilities.UserStatus]: object;
|
||||
[ClientCapabilities.CloudSaves]: object;
|
||||
};
|
||||
|
||||
class CapabilityManager {
|
||||
private validationFunctions: EnumDictionary<
|
||||
InternalClientCapability,
|
||||
ClientCapabilities,
|
||||
(configuration: object) => Promise<boolean>
|
||||
> = {
|
||||
/*
|
||||
@ -77,14 +67,14 @@ class CapabilityManager {
|
||||
return valid;
|
||||
},
|
||||
*/
|
||||
[InternalClientCapability.PeerAPI]: async () => true,
|
||||
[InternalClientCapability.UserStatus]: async () => true, // No requirements for user status
|
||||
[InternalClientCapability.CloudSaves]: async () => true, // No requirements for cloud saves
|
||||
[InternalClientCapability.TrackPlaytime]: async () => true,
|
||||
[ClientCapabilities.PeerAPI]: async () => true,
|
||||
[ClientCapabilities.UserStatus]: async () => true, // No requirements for user status
|
||||
[ClientCapabilities.CloudSaves]: async () => true, // No requirements for cloud saves
|
||||
[ClientCapabilities.TrackPlaytime]: async () => true,
|
||||
};
|
||||
|
||||
async validateCapabilityConfiguration(
|
||||
capability: InternalClientCapability,
|
||||
capability: ClientCapabilities,
|
||||
configuration: object,
|
||||
) {
|
||||
const validationFunction = this.validationFunctions[capability];
|
||||
@ -93,15 +83,15 @@ class CapabilityManager {
|
||||
}
|
||||
|
||||
async upsertClientCapability(
|
||||
capability: InternalClientCapability,
|
||||
capability: ClientCapabilities,
|
||||
rawCapabilityConfiguration: object,
|
||||
clientId: string,
|
||||
) {
|
||||
const upsertFunctions: EnumDictionary<
|
||||
InternalClientCapability,
|
||||
ClientCapabilities,
|
||||
() => Promise<void> | void
|
||||
> = {
|
||||
[InternalClientCapability.PeerAPI]: async function () {
|
||||
[ClientCapabilities.PeerAPI]: async function () {
|
||||
// const configuration =rawCapability as CapabilityConfiguration[InternalClientCapability.PeerAPI];
|
||||
|
||||
const currentClient = await prisma.client.findUnique({
|
||||
@ -139,10 +129,10 @@ class CapabilityManager {
|
||||
},
|
||||
});
|
||||
},
|
||||
[InternalClientCapability.UserStatus]: function (): Promise<void> | void {
|
||||
[ClientCapabilities.UserStatus]: function (): Promise<void> | void {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
[InternalClientCapability.CloudSaves]: async function () {
|
||||
[ClientCapabilities.CloudSaves]: async function () {
|
||||
const currentClient = await prisma.client.findUnique({
|
||||
where: { id: clientId },
|
||||
select: {
|
||||
@ -162,7 +152,7 @@ class CapabilityManager {
|
||||
},
|
||||
});
|
||||
},
|
||||
[InternalClientCapability.TrackPlaytime]: async function () {
|
||||
[ClientCapabilities.TrackPlaytime]: async function () {
|
||||
const currentClient = await prisma.client.findUnique({
|
||||
where: { id: clientId },
|
||||
select: {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { ClientModel, UserModel } from "~/prisma/client/models";
|
||||
import type { ClientModel, UserModel } from "~~/prisma/client/models";
|
||||
import type { EventHandlerRequest, H3Event } from "h3";
|
||||
import droplet from "@drop-oss/droplet";
|
||||
import prisma from "../db/database";
|
||||
import { useCertificateAuthority } from "~/server/plugins/ca";
|
||||
import { useCertificateAuthority } from "~~/server/plugins/ca";
|
||||
|
||||
export type EventHandlerFunction<T> = (
|
||||
h3: H3Event<EventHandlerRequest>,
|
||||
@ -23,7 +23,7 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
|
||||
if (!header) throw createError({ statusCode: 403 });
|
||||
const [method, ...parts] = header.split(" ");
|
||||
|
||||
let clientId: string;
|
||||
let clientId: string | undefined;
|
||||
switch (method) {
|
||||
case "Debug": {
|
||||
if (!import.meta.dev) throw createError({ statusCode: 403 });
|
||||
@ -31,7 +31,7 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
|
||||
if (!client)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "No clients created.",
|
||||
message: "No clients created.",
|
||||
});
|
||||
clientId = client.id;
|
||||
break;
|
||||
@ -55,7 +55,7 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
|
||||
// We reject the request
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Nonce expired",
|
||||
message: "Nonce expired",
|
||||
});
|
||||
}
|
||||
|
||||
@ -66,21 +66,21 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
|
||||
if (!certBundle)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Invalid client ID",
|
||||
message: "Invalid client ID",
|
||||
});
|
||||
|
||||
const valid = droplet.verifyNonce(certBundle.cert, nonce, signature);
|
||||
if (!valid)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Invalid nonce signature.",
|
||||
message: "Invalid nonce signature.",
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "No authentication",
|
||||
message: "No authentication",
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -88,12 +88,12 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
|
||||
if (clientId === undefined)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to execute authentication pipeline.",
|
||||
message: "Failed to execute authentication pipeline.",
|
||||
});
|
||||
|
||||
async function fetchClient() {
|
||||
const client = await prisma.client.findUnique({
|
||||
where: { id: clientId },
|
||||
where: { id: clientId! },
|
||||
});
|
||||
if (!client)
|
||||
throw new Error(
|
||||
@ -104,7 +104,7 @@ export function defineClientEventHandler<T>(handler: EventHandlerFunction<T>) {
|
||||
|
||||
async function fetchUser() {
|
||||
const client = await prisma.client.findUnique({
|
||||
where: { id: clientId },
|
||||
where: { id: clientId! },
|
||||
select: {
|
||||
user: true,
|
||||
},
|
||||
|
||||
@ -1,23 +1,20 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import prisma from "../db/database";
|
||||
import type { Platform } from "~/prisma/client/enums";
|
||||
import { useCertificateAuthority } from "~/server/plugins/ca";
|
||||
import type { ClientCapabilities, HardwarePlatform } from "~~/prisma/client/enums";
|
||||
import { useCertificateAuthority } from "~~/server/plugins/ca";
|
||||
import type {
|
||||
CapabilityConfiguration,
|
||||
InternalClientCapability,
|
||||
} from "./capabilities";
|
||||
import capabilityManager from "./capabilities";
|
||||
import type { PeerImpl } from "../tasks";
|
||||
import userStatsManager from "~/server/internal/userstats";
|
||||
import userStatsManager from "~~/server/internal/userstats";
|
||||
|
||||
export enum AuthMode {
|
||||
Callback = "callback",
|
||||
Code = "code",
|
||||
}
|
||||
export const AuthModes = ["callback", "code"] as const;
|
||||
export type AuthMode = (typeof AuthModes)[number];
|
||||
|
||||
export interface ClientMetadata {
|
||||
name: string;
|
||||
platform: Platform;
|
||||
platform: HardwarePlatform;
|
||||
capabilities: Partial<CapabilityConfiguration>;
|
||||
mode: AuthMode;
|
||||
}
|
||||
@ -63,9 +60,9 @@ export class ClientHandler {
|
||||
});
|
||||
|
||||
switch (metadata.mode) {
|
||||
case AuthMode.Callback:
|
||||
case "callback":
|
||||
return `/client/authorize/${clientId}`;
|
||||
case AuthMode.Code: {
|
||||
case "code": {
|
||||
const code = randomUUID()
|
||||
.replaceAll(/-/g, "")
|
||||
.slice(0, 7)
|
||||
@ -81,15 +78,15 @@ export class ClientHandler {
|
||||
if (!clientId)
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "Invalid or unknown code.",
|
||||
message: "Invalid or unknown code.",
|
||||
});
|
||||
const metadata = this.temporaryClientTable.get(clientId);
|
||||
if (!metadata)
|
||||
throw createError({ statusCode: 500, statusMessage: "Broken code." });
|
||||
throw createError({ statusCode: 500, message: "Broken code." });
|
||||
if (metadata.peer)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Pre-existing listener for this code.",
|
||||
message: "Pre-existing listener for this code.",
|
||||
});
|
||||
metadata.peer = peer;
|
||||
this.temporaryClientTable.set(clientId, metadata);
|
||||
@ -130,12 +127,12 @@ export class ClientHandler {
|
||||
if (!client)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Corrupted code, please restart the process.",
|
||||
message: "Corrupted code, please restart the process.",
|
||||
});
|
||||
if (!client.peer)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Client has not connected yet. Please try again later.",
|
||||
message: "Client has not connected yet. Please try again later.",
|
||||
});
|
||||
client.peer.send(
|
||||
JSON.stringify({ type: "token", value: `${clientId}/${token}` }),
|
||||
@ -173,7 +170,7 @@ export class ClientHandler {
|
||||
metadata.data.capabilities,
|
||||
)) {
|
||||
await capabilityManager.upsertClientCapability(
|
||||
capability as InternalClientCapability,
|
||||
capability as ClientCapabilities,
|
||||
configuration,
|
||||
client.id,
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ApplicationSettingsModel } from "~/prisma/client/models";
|
||||
import type { ApplicationSettingsModel } from "~~/prisma/client/models";
|
||||
import prisma from "../db/database";
|
||||
|
||||
class ApplicationConfiguration {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PrismaClient } from "~/prisma/client/client";
|
||||
import { PrismaClient } from "~~/prisma/client/client";
|
||||
// import { PrismaPg } from "@prisma/adapter-pg";
|
||||
|
||||
const prismaClientSingleton = () => {
|
||||
|
||||
@ -15,12 +15,13 @@ class DownloadContextManager {
|
||||
}
|
||||
> = new Map();
|
||||
|
||||
async createContext(game: string, versionName: string) {
|
||||
const version = await prisma.gameVersion.findUnique({
|
||||
async createContext(game: string, versionPath: string) {
|
||||
const version = await prisma.version.findFirst({
|
||||
where: {
|
||||
gameId_versionName: {
|
||||
gameId: game,
|
||||
versionName,
|
||||
gameId: game,
|
||||
versionPath,
|
||||
game: {
|
||||
isNot: null,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
@ -38,9 +39,9 @@ class DownloadContextManager {
|
||||
this.contexts.set(contextId, {
|
||||
timeout: new Date(),
|
||||
manifest: JSON.parse(version.dropletManifest as string) as DropManifest,
|
||||
versionName,
|
||||
libraryId: version.game.libraryId!,
|
||||
libraryPath: version.game.libraryPath,
|
||||
versionName: versionPath,
|
||||
libraryId: version.game!.libraryId!,
|
||||
libraryPath: version.game!.libraryPath,
|
||||
});
|
||||
|
||||
return contextId;
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { GameVersionModel } from "~/prisma/client/models";
|
||||
import prisma from "../db/database";
|
||||
import { sum } from "~/utils/array";
|
||||
|
||||
@ -15,11 +14,11 @@ export type DropManifest = {
|
||||
|
||||
export type DropManifestMetadata = {
|
||||
manifest: DropManifest;
|
||||
versionName: string;
|
||||
versionId: string;
|
||||
};
|
||||
|
||||
export type DropGeneratedManifest = DropManifest & {
|
||||
[key: string]: { versionName: string };
|
||||
[key: string]: { versionId: string };
|
||||
};
|
||||
|
||||
class ManifestGenerator {
|
||||
@ -32,7 +31,7 @@ class ManifestGenerator {
|
||||
Object.entries(rootManifest.manifest).map(([key, value]) => {
|
||||
return [
|
||||
key,
|
||||
Object.assign({}, value, { versionName: rootManifest.versionName }),
|
||||
Object.assign({}, value, { versionId: rootManifest.versionId }),
|
||||
];
|
||||
}),
|
||||
);
|
||||
@ -45,7 +44,7 @@ class ManifestGenerator {
|
||||
for (const [filename, chunk] of Object.entries(version.manifest)) {
|
||||
if (manifest[filename]) continue;
|
||||
manifest[filename] = Object.assign({}, chunk, {
|
||||
versionName: version.versionName,
|
||||
versionId: version.versionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -54,14 +53,26 @@ class ManifestGenerator {
|
||||
}
|
||||
|
||||
// Local function because eventual caching
|
||||
async generateManifest(gameId: string, versionName: string) {
|
||||
const versions: GameVersionModel[] = [];
|
||||
async generateManifest(versionId: string) {
|
||||
const versions = [];
|
||||
|
||||
const baseVersion = await prisma.gameVersion.findUnique({
|
||||
where: {
|
||||
gameId_versionName: {
|
||||
gameId: gameId,
|
||||
versionName: versionName,
|
||||
versionId,
|
||||
version: {
|
||||
gameId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
platform: true,
|
||||
version: {
|
||||
select: {
|
||||
gameId: true,
|
||||
dropletManifest: true,
|
||||
versionIndex: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -73,31 +84,42 @@ class ManifestGenerator {
|
||||
// Start at the same index minus one, and keep grabbing them
|
||||
// until we run out or we hit something that isn't a delta
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
for (let i = baseVersion.versionIndex - 1; true; i--) {
|
||||
for (let i = baseVersion.version.versionIndex - 1; true; i--) {
|
||||
const currentVersion = await prisma.gameVersion.findFirst({
|
||||
where: {
|
||||
gameId: gameId,
|
||||
versionIndex: i,
|
||||
platform: baseVersion.platform,
|
||||
version: {
|
||||
gameId: baseVersion.version.gameId!,
|
||||
versionIndex: i,
|
||||
},
|
||||
platform: {
|
||||
id: baseVersion.platform.id,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
version: {
|
||||
select: {
|
||||
dropletManifest: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!currentVersion) return undefined;
|
||||
versions.push(currentVersion);
|
||||
if (!currentVersion.delta) break;
|
||||
if (!currentVersion?.delta) break;
|
||||
}
|
||||
}
|
||||
const leastToMost = versions.reverse();
|
||||
const metadata: DropManifestMetadata[] = leastToMost.map((e) => {
|
||||
versions.reverse();
|
||||
const metadata: DropManifestMetadata[] = versions.map((gameVersion) => {
|
||||
return {
|
||||
manifest: JSON.parse(
|
||||
e.dropletManifest?.toString() ?? "{}",
|
||||
gameVersion.version.dropletManifest?.toString() ?? "{}",
|
||||
) as DropManifest,
|
||||
versionName: e.versionName,
|
||||
versionId: gameVersion.versionId,
|
||||
};
|
||||
});
|
||||
|
||||
const manifest = ManifestGenerator.generateManifestFromMetadata(
|
||||
metadata[0],
|
||||
metadata[0]!,
|
||||
...metadata.slice(1),
|
||||
);
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { sum } from "~/utils/array";
|
||||
import cacheHandler from "../cache";
|
||||
import prisma from "../db/database";
|
||||
import manifestGenerator from "../downloads/manifest";
|
||||
import { sum } from "../../../utils/array";
|
||||
import type { Game, GameVersion } from "~/prisma/client/client";
|
||||
import type { Game, Version } from "~~/prisma/client/client";
|
||||
|
||||
export type GameSize = {
|
||||
gameName: string;
|
||||
@ -15,7 +15,7 @@ export type VersionSize = GameSize & {
|
||||
};
|
||||
|
||||
type VersionsSizes = {
|
||||
[versionName: string]: VersionSize;
|
||||
[versionId: string]: VersionSize;
|
||||
};
|
||||
|
||||
type GameVersionsSize = {
|
||||
@ -42,7 +42,7 @@ class GameSizeManager {
|
||||
|
||||
// All versions of a game combined
|
||||
async getCombinedGameSize(gameId: string) {
|
||||
const versions = await prisma.gameVersion.findMany({
|
||||
const versions = await prisma.version.findMany({
|
||||
where: { gameId },
|
||||
});
|
||||
const sizes = await Promise.all(
|
||||
@ -57,10 +57,10 @@ class GameSizeManager {
|
||||
|
||||
async getGameVersionSize(
|
||||
gameId: string,
|
||||
versionName?: string,
|
||||
versionId?: string,
|
||||
): Promise<number | null> {
|
||||
if (!versionName) {
|
||||
const version = await prisma.gameVersion.findFirst({
|
||||
if (!versionId) {
|
||||
const version = await prisma.version.findFirst({
|
||||
where: { gameId },
|
||||
orderBy: {
|
||||
versionIndex: "desc",
|
||||
@ -69,13 +69,10 @@ class GameSizeManager {
|
||||
if (!version) {
|
||||
return null;
|
||||
}
|
||||
versionName = version.versionName;
|
||||
versionId = version.versionId;
|
||||
}
|
||||
|
||||
const manifest = await manifestGenerator.generateManifest(
|
||||
gameId,
|
||||
versionName,
|
||||
);
|
||||
const manifest = await manifestGenerator.generateManifest(versionId);
|
||||
if (!manifest) {
|
||||
return null;
|
||||
}
|
||||
@ -84,11 +81,11 @@ class GameSizeManager {
|
||||
}
|
||||
|
||||
private async isLatestVersion(
|
||||
gameVersions: GameVersion[],
|
||||
version: GameVersion,
|
||||
gameVersions: Version[],
|
||||
version: Version,
|
||||
): Promise<boolean> {
|
||||
return gameVersions.length > 0
|
||||
? gameVersions[0].versionName === version.versionName
|
||||
? gameVersions[0].versionId === version.versionId
|
||||
: false;
|
||||
}
|
||||
|
||||
@ -101,7 +98,7 @@ class GameSizeManager {
|
||||
return null;
|
||||
}
|
||||
const latestVersionName = Object.keys(versionsSizes).find(
|
||||
(versionName) => versionsSizes[versionName].latest,
|
||||
(versionId) => versionsSizes[versionId].latest,
|
||||
);
|
||||
if (!latestVersionName) {
|
||||
return null;
|
||||
@ -161,17 +158,17 @@ class GameSizeManager {
|
||||
}
|
||||
|
||||
async cacheGameVersion(
|
||||
game: Game & { versions: GameVersion[] },
|
||||
versionName?: string,
|
||||
game: Game & { versions: Version[] },
|
||||
versionId?: string,
|
||||
) {
|
||||
const cacheVersion = async (version: GameVersion) => {
|
||||
const size = await this.getGameVersionSize(game.id, version.versionName);
|
||||
if (!version.versionName || !size) {
|
||||
const cacheVersion = async (version: Version) => {
|
||||
const size = await this.getGameVersionSize(game.id, version.versionId);
|
||||
if (!version.versionId || !size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const versionsSizes = {
|
||||
[version.versionName]: {
|
||||
[version.versionId]: {
|
||||
size,
|
||||
gameName: game.mName,
|
||||
gameId: game.id,
|
||||
@ -186,9 +183,9 @@ class GameSizeManager {
|
||||
});
|
||||
};
|
||||
|
||||
if (versionName) {
|
||||
const version = await prisma.gameVersion.findFirst({
|
||||
where: { gameId: game.id, versionName },
|
||||
if (versionId) {
|
||||
const version = await prisma.version.findFirst({
|
||||
where: { gameId: game.id, versionId },
|
||||
});
|
||||
if (!version) {
|
||||
return;
|
||||
@ -212,7 +209,7 @@ class GameSizeManager {
|
||||
.slice(0, top);
|
||||
}
|
||||
|
||||
async deleteGameVersion(gameId: string, version: string) {
|
||||
async deleteGameVersion(gameId: string, versionId: string) {
|
||||
const game = await prisma.game.findFirst({ where: { id: gameId } });
|
||||
if (game) {
|
||||
await this.cacheCombinedGame(game);
|
||||
@ -222,7 +219,7 @@ class GameSizeManager {
|
||||
return;
|
||||
}
|
||||
// Remove the version from the VersionsSizes object
|
||||
const { [version]: _, ...updatedVersionsSizes } = versionsSizes;
|
||||
const { [versionId]: _, ...updatedVersionsSizes } = versionsSizes;
|
||||
await this.gameVersionsSizesCache.set(gameId, updatedVersionsSizes);
|
||||
}
|
||||
|
||||
@ -232,5 +229,5 @@ class GameSizeManager {
|
||||
}
|
||||
}
|
||||
|
||||
export const manager = new GameSizeManager();
|
||||
export default manager;
|
||||
export const gameSizeManager = new GameSizeManager();
|
||||
export default gameSizeManager;
|
||||
|
||||
3
server/internal/library/REDIST-README.md
Normal file
3
server/internal/library/REDIST-README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Redistributables
|
||||
|
||||
They are called 'redist' in the codebase/database models for brevity. Additionally, because they intentionally are drop-ins for games, they are added to all the functions that deal with games, without changing the names.
|
||||
@ -9,14 +9,29 @@ 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";
|
||||
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 type { ImportVersion } from "~~/server/api/v1/admin/import/version/index.post";
|
||||
import type {
|
||||
GameVersionCreateInput,
|
||||
LaunchOptionCreateManyInput,
|
||||
VersionCreateInput,
|
||||
VersionWhereInput,
|
||||
} from "~~/prisma/client/models";
|
||||
import type { PlatformLink } from "~~/prisma/client/client";
|
||||
import { convertIDToLink } from "../platform/link";
|
||||
import type { WorkingLibrarySource } from "~~/server/api/v1/admin/library/sources/index.get";
|
||||
import gameSizeManager from "../gamesize";
|
||||
|
||||
export const VersionImportModes = ["game", "redist"] as const;
|
||||
export type VersionImportMode = (typeof VersionImportModes)[number];
|
||||
|
||||
const modeToLink: { [key in VersionImportMode]: string } = {
|
||||
game: "g",
|
||||
redist: "r",
|
||||
};
|
||||
|
||||
export function createGameImportTaskId(libraryId: string, libraryPath: string) {
|
||||
return createHash("md5")
|
||||
@ -57,14 +72,15 @@ class LibraryManager {
|
||||
}
|
||||
|
||||
async fetchGamesByLibrary() {
|
||||
const results: { [key: string]: { [key: string]: GameModel } } = {};
|
||||
const results: { [key: string]: { [key: string]: boolean } } = {};
|
||||
const games = await prisma.game.findMany({});
|
||||
for (const game of games) {
|
||||
const libraryId = game.libraryId!;
|
||||
const libraryPath = game.libraryPath!;
|
||||
const redist = await prisma.redist.findMany({});
|
||||
for (const item of [...games, ...redist]) {
|
||||
const libraryId = item.libraryId!;
|
||||
const libraryPath = item.libraryPath!;
|
||||
|
||||
results[libraryId] ??= {};
|
||||
results[libraryId][libraryPath] = game;
|
||||
results[libraryId][libraryPath] = true;
|
||||
}
|
||||
|
||||
return results;
|
||||
@ -90,18 +106,31 @@ class LibraryManager {
|
||||
async fetchUnimportedGameVersions(libraryId: string, libraryPath: string) {
|
||||
const provider = this.libraries.get(libraryId);
|
||||
if (!provider) return undefined;
|
||||
const game = await prisma.game.findUnique({
|
||||
where: {
|
||||
libraryKey: {
|
||||
libraryId,
|
||||
libraryPath,
|
||||
const game =
|
||||
(await prisma.game.findUnique({
|
||||
where: {
|
||||
libraryKey: {
|
||||
libraryId,
|
||||
libraryPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
versions: true,
|
||||
},
|
||||
});
|
||||
select: {
|
||||
id: true,
|
||||
versions: true,
|
||||
},
|
||||
})) ??
|
||||
(await prisma.redist.findUnique({
|
||||
where: {
|
||||
libraryKey: {
|
||||
libraryId,
|
||||
libraryPath,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
versions: true,
|
||||
},
|
||||
}));
|
||||
if (!game) return undefined;
|
||||
|
||||
try {
|
||||
@ -121,29 +150,23 @@ class LibraryManager {
|
||||
}
|
||||
}
|
||||
|
||||
async fetchGamesWithStatus() {
|
||||
const games = await prisma.game.findMany({
|
||||
include: {
|
||||
versions: {
|
||||
select: {
|
||||
versionName: true,
|
||||
},
|
||||
},
|
||||
library: true,
|
||||
},
|
||||
orderBy: {
|
||||
mName: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
async fetchLibraryObjectWithStatus<T>(
|
||||
objects: Array<
|
||||
{
|
||||
libraryId: string;
|
||||
libraryPath: string;
|
||||
versions: Array<unknown>;
|
||||
} & T
|
||||
>,
|
||||
) {
|
||||
return await Promise.all(
|
||||
games.map(async (e) => {
|
||||
objects.map(async (e) => {
|
||||
const versions = await this.fetchUnimportedGameVersions(
|
||||
e.libraryId ?? "",
|
||||
e.libraryPath,
|
||||
);
|
||||
return {
|
||||
game: e,
|
||||
value: e,
|
||||
status: versions
|
||||
? {
|
||||
noVersions: e.versions.length == 0,
|
||||
@ -155,22 +178,220 @@ class LibraryManager {
|
||||
);
|
||||
}
|
||||
|
||||
async fetchGamesWithStatus() {
|
||||
const games = await prisma.game.findMany({
|
||||
include: {
|
||||
versions: {
|
||||
select: {
|
||||
versionId: true,
|
||||
versionName: true,
|
||||
},
|
||||
},
|
||||
library: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
mName: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
return await this.fetchLibraryObjectWithStatus(games);
|
||||
}
|
||||
|
||||
async fetchRedistsWithStatus() {
|
||||
const redists = await prisma.redist.findMany({
|
||||
include: {
|
||||
versions: {
|
||||
select: {
|
||||
versionId: true,
|
||||
versionName: true,
|
||||
},
|
||||
},
|
||||
library: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
platform: true,
|
||||
},
|
||||
orderBy: {
|
||||
mName: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
return await this.fetchLibraryObjectWithStatus(redists);
|
||||
}
|
||||
|
||||
private async fetchLibraryPath(
|
||||
id: string,
|
||||
mode: VersionImportMode,
|
||||
platform?: PlatformLink,
|
||||
): Promise<
|
||||
| [
|
||||
{ mName: string; libraryId: string; libraryPath: string } | null,
|
||||
VersionWhereInput,
|
||||
]
|
||||
| undefined
|
||||
> {
|
||||
switch (mode) {
|
||||
case "game":
|
||||
return [
|
||||
await prisma.game.findUnique({
|
||||
where: { id },
|
||||
select: { mName: true, libraryId: true, libraryPath: true },
|
||||
}),
|
||||
{ gameId: id, gameVersions: { some: { platform } } },
|
||||
];
|
||||
case "redist":
|
||||
return [
|
||||
await prisma.redist.findUnique({
|
||||
where: { id },
|
||||
select: { mName: true, libraryId: true, libraryPath: true },
|
||||
}),
|
||||
{ redistId: id },
|
||||
];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private createVersionOptions(
|
||||
id: string,
|
||||
currentIndex: number,
|
||||
metadata: typeof ImportVersion.infer,
|
||||
): Omit<
|
||||
VersionCreateInput,
|
||||
"versionPath" | "versionName" | "dropletManifest"
|
||||
> {
|
||||
const installCreator = {
|
||||
install: {
|
||||
create: {
|
||||
name: "",
|
||||
description: "",
|
||||
command: metadata.install!,
|
||||
args: metadata.installArgs || "",
|
||||
},
|
||||
},
|
||||
} satisfies Partial<GameVersionCreateInput>;
|
||||
|
||||
const uninstallCreator = {
|
||||
uninstall: {
|
||||
create: {
|
||||
name: "",
|
||||
description: "",
|
||||
command: metadata.uninstall!,
|
||||
args: metadata.uninstallArgs || "",
|
||||
},
|
||||
},
|
||||
} satisfies Partial<GameVersionCreateInput>;
|
||||
|
||||
switch (metadata.mode) {
|
||||
case "game": {
|
||||
return {
|
||||
versionIndex: currentIndex,
|
||||
game: {
|
||||
connect: {
|
||||
id,
|
||||
},
|
||||
},
|
||||
gameVersions: {
|
||||
create: {
|
||||
delta: metadata.delta,
|
||||
umuIdOverride: metadata.umuId,
|
||||
|
||||
onlySetup: metadata.onlySetup,
|
||||
|
||||
launches: {
|
||||
createMany: {
|
||||
data: metadata.launches.map(
|
||||
(v) =>
|
||||
({
|
||||
name: v.name,
|
||||
description: v.description,
|
||||
command: v.launchCommand,
|
||||
args: v.launchArgs,
|
||||
}) satisfies LaunchOptionCreateManyInput,
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
...(metadata.install ? installCreator : undefined),
|
||||
...(metadata.uninstall ? uninstallCreator : undefined),
|
||||
|
||||
platform: {
|
||||
connect: {
|
||||
id: metadata.platform,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case "redist":
|
||||
return {
|
||||
versionIndex: currentIndex,
|
||||
redist: {
|
||||
connect: {
|
||||
id,
|
||||
},
|
||||
},
|
||||
redistVersions: {
|
||||
create: {
|
||||
versionIndex: currentIndex,
|
||||
delta: metadata.delta,
|
||||
|
||||
launches: {
|
||||
createMany: {
|
||||
data: metadata.launches.map(
|
||||
(v) =>
|
||||
({
|
||||
name: v.name,
|
||||
description: v.description,
|
||||
command: v.launchCommand,
|
||||
args: v.launchArgs,
|
||||
}) satisfies LaunchOptionCreateManyInput,
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
...(metadata.install ? installCreator : undefined),
|
||||
...(metadata.uninstall ? uninstallCreator : undefined),
|
||||
|
||||
platform: {
|
||||
connect: {
|
||||
id: metadata.platform,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches recommendations and extra data about the version. Doesn't actually check if it's been imported.
|
||||
* @param gameId
|
||||
* @param versionName
|
||||
* @param id
|
||||
* @param version
|
||||
* @returns
|
||||
*/
|
||||
async fetchUnimportedVersionInformation(gameId: string, versionName: string) {
|
||||
const game = await prisma.game.findUnique({
|
||||
where: { id: gameId },
|
||||
select: { libraryPath: true, libraryId: true, mName: true },
|
||||
});
|
||||
if (!game || !game.libraryId) return undefined;
|
||||
async fetchUnimportedVersionInformation(
|
||||
id: string,
|
||||
mode: VersionImportMode,
|
||||
version: string,
|
||||
) {
|
||||
const value = await this.fetchLibraryPath(id, mode);
|
||||
if (!value?.[0] || !value[0].libraryId) return undefined;
|
||||
const [libraryDetails] = value;
|
||||
|
||||
const library = this.libraries.get(game.libraryId);
|
||||
const library = this.libraries.get(libraryDetails.libraryId);
|
||||
if (!library) return undefined;
|
||||
|
||||
const userPlatforms = await prisma.userPlatform.findMany({});
|
||||
|
||||
const fileExts: { [key: string]: string[] } = {
|
||||
Linux: [
|
||||
// Ext for Unity games
|
||||
@ -189,13 +410,20 @@ class LibraryManager {
|
||||
],
|
||||
};
|
||||
|
||||
for (const platform of userPlatforms) {
|
||||
fileExts[platform.id] = platform.fileExtensions;
|
||||
}
|
||||
|
||||
const options: Array<{
|
||||
filename: string;
|
||||
platform: string;
|
||||
match: number;
|
||||
}> = [];
|
||||
|
||||
const files = await library.versionReaddir(game.libraryPath, versionName);
|
||||
const files = await library.versionReaddir(
|
||||
libraryDetails.libraryPath,
|
||||
version,
|
||||
);
|
||||
for (const filename of files) {
|
||||
const basename = path.basename(filename);
|
||||
const dotLocation = filename.lastIndexOf(".");
|
||||
@ -204,7 +432,7 @@ class LibraryManager {
|
||||
for (const [platform, checkExts] of Object.entries(fileExts)) {
|
||||
for (const checkExt of checkExts) {
|
||||
if (checkExt != ext) continue;
|
||||
const fuzzyValue = fuzzy(basename, game.mName);
|
||||
const fuzzyValue = fuzzy(basename, libraryDetails.mName);
|
||||
options.push({
|
||||
filename,
|
||||
platform,
|
||||
@ -227,6 +455,10 @@ class LibraryManager {
|
||||
})) > 0;
|
||||
if (hasGame) return false;
|
||||
|
||||
const hasRedist =
|
||||
(await prisma.redist.count({ where: { libraryId, libraryPath } })) > 0;
|
||||
if (hasRedist) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -239,46 +471,70 @@ 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;
|
||||
},
|
||||
id: string,
|
||||
version: string,
|
||||
metadata: typeof ImportVersion.infer,
|
||||
) {
|
||||
const taskId = createVersionImportTaskId(gameId, versionName);
|
||||
const taskId = createVersionImportTaskId(id, version);
|
||||
|
||||
const platform = parsePlatform(metadata.platform);
|
||||
if (!platform) return undefined;
|
||||
if (metadata.mode === "game") {
|
||||
if (metadata.onlySetup) {
|
||||
if (!metadata.install)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: "An install command is required in only-setup mode.",
|
||||
});
|
||||
} else {
|
||||
if (!metadata.delta && metadata.launches.length == 0)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message:
|
||||
"At least one launch command is required in non-delta, non-setup mode.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const game = await prisma.game.findUnique({
|
||||
where: { id: gameId },
|
||||
select: { mName: true, libraryId: true, libraryPath: true },
|
||||
const platform = await convertIDToLink(metadata.platform);
|
||||
if (!platform)
|
||||
throw createError({ statusCode: 400, message: "Invalid platform." });
|
||||
|
||||
const value = await this.fetchLibraryPath(id, metadata.mode, platform);
|
||||
if (!value || !value[0])
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: `${metadata.mode} not found.`,
|
||||
});
|
||||
const [libraryDetails, idFilter] = value;
|
||||
|
||||
const library = this.libraries.get(libraryDetails.libraryId);
|
||||
if (!library)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: "Library not found but exists in database?",
|
||||
});
|
||||
|
||||
const currentIndex = await prisma.version.count({
|
||||
where: { ...idFilter },
|
||||
});
|
||||
if (!game || !game.libraryId) return undefined;
|
||||
|
||||
const library = this.libraries.get(game.libraryId);
|
||||
if (!library) return undefined;
|
||||
if (metadata.delta && currentIndex == 0)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message:
|
||||
"At least one pre-existing version of the same platform is required for delta mode.",
|
||||
});
|
||||
|
||||
taskHandler.create({
|
||||
id: taskId,
|
||||
taskGroup: "import:game",
|
||||
name: `Importing version ${versionName} for ${game.mName}`,
|
||||
name: `Importing version "${metadata.name}" (${version}) for ${libraryDetails.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,
|
||||
libraryDetails.libraryPath,
|
||||
version,
|
||||
(err, value) => {
|
||||
if (err) throw err;
|
||||
progress(value * 0.9);
|
||||
@ -291,59 +547,35 @@ class LibraryManager {
|
||||
|
||||
logger.info("Created manifest successfully!");
|
||||
|
||||
const currentIndex = await prisma.gameVersion.count({
|
||||
where: { gameId: gameId },
|
||||
});
|
||||
|
||||
// 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,
|
||||
const createdVersion = await prisma.version.create({
|
||||
data: {
|
||||
versionPath: version,
|
||||
versionName: metadata.name ?? version,
|
||||
dropletManifest: manifest,
|
||||
|
||||
onlySetup: true,
|
||||
setupCommand: metadata.setup,
|
||||
setupArgs: metadata.setupArgs.split(" "),
|
||||
},
|
||||
});
|
||||
} 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(" "),
|
||||
},
|
||||
});
|
||||
}
|
||||
...libraryManager.createVersionOptions(id, currentIndex, metadata),
|
||||
},
|
||||
});
|
||||
|
||||
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}.`,
|
||||
actions: [`View|/admin/library/${gameId}`],
|
||||
nonce: `version-create-${id}-${version}`,
|
||||
title: `'${libraryDetails.mName}' ('${version}') finished importing.`,
|
||||
description: `Drop finished importing version ${version} for ${libraryDetails.mName}.`,
|
||||
actions: [`View|/admin/library/${modeToLink[metadata.mode]}/${id}`],
|
||||
acls: ["system:import:version:read"],
|
||||
});
|
||||
|
||||
await libraryManager.cacheCombinedGameSize(gameId);
|
||||
await libraryManager.cacheGameVersionSize(gameId, versionName);
|
||||
if (metadata.mode === "game") {
|
||||
await libraryManager.cacheCombinedGameSize(id);
|
||||
await libraryManager.cacheGameVersionSize(
|
||||
id,
|
||||
createdVersion.versionId,
|
||||
);
|
||||
}
|
||||
|
||||
progress(100);
|
||||
},
|
||||
});
|
||||
@ -374,17 +606,22 @@ class LibraryManager {
|
||||
return await library.readFile(game, version, filename, options);
|
||||
}
|
||||
|
||||
async deleteGameVersion(gameId: string, version: string) {
|
||||
await prisma.gameVersion.delete({
|
||||
async deleteGameVersion(versionId: string) {
|
||||
const version = await prisma.version.delete({
|
||||
where: {
|
||||
gameId_versionName: {
|
||||
gameId: gameId,
|
||||
versionName: version,
|
||||
},
|
||||
versionId,
|
||||
},
|
||||
include: {
|
||||
game: true,
|
||||
},
|
||||
});
|
||||
|
||||
await gameSizeManager.deleteGameVersion(gameId, version);
|
||||
if (version.game) {
|
||||
await gameSizeManager.deleteGameVersion(
|
||||
version.game.id,
|
||||
version.versionId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteGame(gameId: string) {
|
||||
@ -398,9 +635,9 @@ class LibraryManager {
|
||||
|
||||
async getGameVersionSize(
|
||||
gameId: string,
|
||||
versionName?: string,
|
||||
versionId?: string,
|
||||
): Promise<number | null> {
|
||||
return gameSizeManager.getGameVersionSize(gameId, versionName);
|
||||
return gameSizeManager.getGameVersionSize(gameId, versionId);
|
||||
}
|
||||
|
||||
async getBiggestGamesCombinedVersions(top: number) {
|
||||
@ -425,7 +662,7 @@ class LibraryManager {
|
||||
await gameSizeManager.cacheCombinedGame(game);
|
||||
}
|
||||
|
||||
async cacheGameVersionSize(gameId: string, versionName: string) {
|
||||
async cacheGameVersionSize(gameId: string, versionId: string) {
|
||||
const game = await prisma.game.findFirst({
|
||||
where: { id: gameId },
|
||||
include: { versions: true },
|
||||
@ -433,7 +670,7 @@ class LibraryManager {
|
||||
if (!game) {
|
||||
return;
|
||||
}
|
||||
await gameSizeManager.cacheGameVersion(game, versionName);
|
||||
await gameSizeManager.cacheGameVersion(game, versionId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { LibraryBackend } from "~/prisma/client/enums";
|
||||
import type { LibraryBackend } from "~~/prisma/client/enums";
|
||||
|
||||
export abstract class LibraryProvider<CFG> {
|
||||
constructor(_config: CFG, _id: string) {
|
||||
|
||||
@ -4,11 +4,11 @@ import {
|
||||
VersionNotFoundError,
|
||||
type LibraryProvider,
|
||||
} from "../provider";
|
||||
import { LibraryBackend } from "~/prisma/client/enums";
|
||||
import { LibraryBackend } from "~~/prisma/client/enums";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import droplet, { DropletHandler } from "@drop-oss/droplet";
|
||||
import { fsStats } from "~/server/internal/utils/files";
|
||||
import { fsStats } from "~~/server/internal/utils/files";
|
||||
|
||||
export const FilesystemProviderConfig = type({
|
||||
baseDir: "string",
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { ArkErrors, type } from "arktype";
|
||||
import type { LibraryProvider } from "../provider";
|
||||
import { VersionNotFoundError } from "../provider";
|
||||
import { LibraryBackend } from "~/prisma/client/enums";
|
||||
import { LibraryBackend } from "~~/prisma/client/enums";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import droplet from "@drop-oss/droplet";
|
||||
import { DROPLET_HANDLER } from "./filesystem";
|
||||
import { fsStats } from "~/server/internal/utils/files";
|
||||
import { fsStats } from "~~/server/internal/utils/files";
|
||||
|
||||
export const FlatFilesystemProviderConfig = type({
|
||||
baseDir: "string",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { CompanyModel } from "~/prisma/client/models";
|
||||
import { MetadataSource } from "~/prisma/client/enums";
|
||||
import type { CompanyModel } from "~~/prisma/client/models";
|
||||
import { MetadataSource } from "~~/prisma/client/enums";
|
||||
import type { MetadataProvider } from ".";
|
||||
import { MissingMetadataProviderConfig } from ".";
|
||||
import type {
|
||||
@ -13,7 +13,7 @@ import type {
|
||||
import axios, { type AxiosRequestConfig } from "axios";
|
||||
import TurndownService from "turndown";
|
||||
import { DateTime } from "luxon";
|
||||
import type { TaskRunContext } from "../tasks";
|
||||
import type { TaskRunContext } from "../tasks/utils";
|
||||
|
||||
interface GiantBombResponseType<T> {
|
||||
error: "OK" | string;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { CompanyModel } from "~/prisma/client/models";
|
||||
import { MetadataSource } from "~/prisma/client/enums";
|
||||
import type { CompanyModel } from "~~/prisma/client/models";
|
||||
import { MetadataSource } from "~~/prisma/client/enums";
|
||||
import type { MetadataProvider } from ".";
|
||||
import { MissingMetadataProviderConfig } from ".";
|
||||
import type {
|
||||
@ -13,8 +13,8 @@ import type { AxiosRequestConfig } from "axios";
|
||||
import axios from "axios";
|
||||
import { DateTime } from "luxon";
|
||||
import * as jdenticon from "jdenticon";
|
||||
import type { TaskRunContext } from "../tasks";
|
||||
import { logger } from "~/server/internal/logging";
|
||||
import { logger } from "~~/server/internal/logging";
|
||||
import type { TaskRunContext } from "../tasks/utils";
|
||||
|
||||
type IGDBID = number;
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Prisma } from "~/prisma/client/client";
|
||||
import { MetadataSource } from "~/prisma/client/enums";
|
||||
import type { Prisma } from "~~/prisma/client/client";
|
||||
import { MetadataSource } from "~~/prisma/client/enums";
|
||||
import prisma from "../db/database";
|
||||
import type {
|
||||
_FetchGameMetadataParams,
|
||||
@ -13,13 +13,14 @@ import type {
|
||||
import { ObjectTransactionalHandler } from "../objects/transactional";
|
||||
import { PriorityListIndexed } from "../utils/prioritylist";
|
||||
import { systemConfig } from "../config/sys-conf";
|
||||
import type { TaskRunContext } from "../tasks";
|
||||
import taskHandler, { wrapTaskContext } from "../tasks";
|
||||
import { randomUUID } from "crypto";
|
||||
import { fuzzy } from "fast-fuzzy";
|
||||
import { logger } from "~/server/internal/logging";
|
||||
import { createGameImportTaskId } from "../library";
|
||||
import type { GameTagModel } from "~/prisma/client/models";
|
||||
import { logger } from "~~/server/internal/logging";
|
||||
import libraryManager, { createGameImportTaskId } from "../library";
|
||||
import type { GameTagModel } from "~~/prisma/client/models";
|
||||
import type { TaskRunContext} from "../tasks/utils";
|
||||
import { wrapTaskContext } from "../tasks/utils";
|
||||
import taskHandler from "../tasks";
|
||||
|
||||
export class MissingMetadataProviderConfig extends Error {
|
||||
private providerName: string;
|
||||
@ -175,15 +176,8 @@ export class MetadataHandler {
|
||||
if (!provider)
|
||||
throw new Error(`Invalid metadata provider for ID "${result.sourceId}"`);
|
||||
|
||||
const existing = await prisma.game.findUnique({
|
||||
where: {
|
||||
metadataKey: {
|
||||
metadataSource: provider.source(),
|
||||
metadataId: result.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (existing) return undefined;
|
||||
const okay = await libraryManager.checkUnimportedGamePath(libraryId, libraryPath);
|
||||
if (!okay) return undefined;
|
||||
|
||||
const gameId = randomUUID();
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { MetadataSource } from "~/prisma/client/enums";
|
||||
import { MetadataSource } from "~~/prisma/client/enums";
|
||||
import type { MetadataProvider } from ".";
|
||||
import type {
|
||||
_FetchGameMetadataParams,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { CompanyModel } from "~/prisma/client/models";
|
||||
import { MetadataSource } from "~/prisma/client/enums";
|
||||
import type { CompanyModel } from "~~/prisma/client/models";
|
||||
import { MetadataSource } from "~~/prisma/client/enums";
|
||||
import type { MetadataProvider } from ".";
|
||||
import type {
|
||||
GameMetadataSearchResult,
|
||||
@ -15,8 +15,8 @@ import * as jdenticon from "jdenticon";
|
||||
import { DateTime } from "luxon";
|
||||
import * as cheerio from "cheerio";
|
||||
import { type } from "arktype";
|
||||
import type { TaskRunContext } from "../tasks";
|
||||
import { logger } from "~/server/internal/logging";
|
||||
import { logger } from "~~/server/internal/logging";
|
||||
import type { TaskRunContext } from "../tasks/utils";
|
||||
|
||||
interface PCGamingWikiParseRawPage {
|
||||
parse: {
|
||||
@ -200,7 +200,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
|
||||
return url.pathname.replace("/games/", "").replace(/\/$/, "");
|
||||
}
|
||||
default: {
|
||||
logger.warn("Pcgamingwiki, unknown host", url.hostname);
|
||||
logger.warn("Pcgamingwiki, unknown host: %s", url.hostname);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@ -234,7 +234,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
|
||||
});
|
||||
if (ratingObj instanceof type.errors) {
|
||||
logger.info(
|
||||
"pcgamingwiki: failed to properly get review rating",
|
||||
"pcgamingwiki: failed to properly get review rating: %s",
|
||||
ratingObj.summary,
|
||||
);
|
||||
return undefined;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { MetadataSource } from "~/prisma/client/enums";
|
||||
import { MetadataSource } from "~~/prisma/client/enums";
|
||||
import type { MetadataProvider } from ".";
|
||||
import type {
|
||||
GameMetadataSearchResult,
|
||||
@ -8,9 +8,9 @@ import type {
|
||||
CompanyMetadata,
|
||||
GameMetadataRating,
|
||||
} from "./types";
|
||||
import type { TaskRunContext } from "../tasks";
|
||||
import axios from "axios";
|
||||
import * as jdenticon from "jdenticon";
|
||||
import type { TaskRunContext } from "../tasks/utils";
|
||||
|
||||
/**
|
||||
* Note: The Steam API is largely undocumented.
|
||||
|
||||
2
server/internal/metadata/types.d.ts
vendored
2
server/internal/metadata/types.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
import type { Company, GameRating } from "~/prisma/client";
|
||||
import type { Company, GameRating } from "~~/prisma/client";
|
||||
import type { TransactionDataType } from "../objects/transactional";
|
||||
import type { ObjectReference } from "../objects/objectHandler";
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ Design goals:
|
||||
2. Real-time; use websocket listeners to keep clients up-to-date
|
||||
*/
|
||||
|
||||
import type { NotificationModel } from "~/prisma/client/models";
|
||||
import type { NotificationModel } from "~~/prisma/client/models";
|
||||
import prisma from "../db/database";
|
||||
import type { GlobalACL } from "../acls";
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import prisma from "../db/database";
|
||||
import cacheHandler from "../cache";
|
||||
import { systemConfig } from "../config/sys-conf";
|
||||
import { type } from "arktype";
|
||||
import { logger } from "~/server/internal/logging";
|
||||
import { logger } from "~~/server/internal/logging";
|
||||
import type pino from "pino";
|
||||
|
||||
export class FsObjectBackend extends ObjectBackend {
|
||||
@ -123,7 +123,7 @@ export class FsObjectBackend extends ObjectBackend {
|
||||
const metadataRaw = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
|
||||
const metadata = objectMetadata(metadataRaw);
|
||||
if (metadata instanceof type.errors) {
|
||||
logger.error("FsObjectBackend#fetchMetadata", metadata.summary);
|
||||
logger.error("FsObjectBackend#fetchMetadata: %s", metadata.summary);
|
||||
return undefined;
|
||||
}
|
||||
await this.metadataCache.set(id, metadata);
|
||||
@ -194,11 +194,13 @@ export class FsObjectBackend extends ObjectBackend {
|
||||
try {
|
||||
fs.rmSync(filePath);
|
||||
cleanupLogger.info(
|
||||
`[FsObjectBackend#cleanupMetadata]: Removed ${file}`,
|
||||
`[FsObjectBackend#cleanupMetadata]: Removed %s`,
|
||||
file
|
||||
);
|
||||
} catch (error) {
|
||||
cleanupLogger.error(
|
||||
`[FsObjectBackend#cleanupMetadata]: Failed to remove ${file}`,
|
||||
`[FsObjectBackend#cleanupMetadata]: Failed to remove %s: %s`,
|
||||
file,
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
@ -32,15 +32,12 @@ export const objectMetadata = type({
|
||||
});
|
||||
export type ObjectMetadata = typeof objectMetadata.infer;
|
||||
|
||||
export enum ObjectPermission {
|
||||
Read = "read",
|
||||
Write = "write",
|
||||
Delete = "delete",
|
||||
}
|
||||
export const ObjectPermissions = ["read", "write", "delete"] as const;
|
||||
export type ObjectPermission = (typeof ObjectPermissions)[number];
|
||||
export const ObjectPermissionPriority: Array<ObjectPermission> = [
|
||||
ObjectPermission.Read,
|
||||
ObjectPermission.Write,
|
||||
ObjectPermission.Delete,
|
||||
"read",
|
||||
"write",
|
||||
"delete",
|
||||
];
|
||||
|
||||
export type Object = { mime: string; data: Source };
|
||||
|
||||
@ -5,7 +5,7 @@ This is used as a utility in metadata handling, so we only fetch the objects if
|
||||
import type { Readable } from "stream";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import objectHandler from ".";
|
||||
import type { TaskRunContext } from "../tasks";
|
||||
import type { TaskRunContext } from "../tasks/utils";
|
||||
|
||||
export type TransactionDataType = string | Readable | Buffer;
|
||||
type TransactionTable = Map<string, TransactionDataType>; // ID to data
|
||||
|
||||
47
server/internal/platform/link.ts
Normal file
47
server/internal/platform/link.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { HardwarePlatform } from "~~/prisma/client/enums";
|
||||
import prisma from "../db/database";
|
||||
import type { PlatformLink } from "~~/prisma/client/client";
|
||||
|
||||
export async function convertIDsToPlatforms(platformIDs: string[]) {
|
||||
const userPlatforms = await prisma.userPlatform.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: platformIDs,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const platforms = platformIDs.map(
|
||||
(e) => userPlatforms.find((v) => v.id === e) ?? (e as HardwarePlatform),
|
||||
);
|
||||
|
||||
return platforms;
|
||||
}
|
||||
|
||||
export async function convertIDToLink(
|
||||
id: string,
|
||||
): Promise<PlatformLink | undefined> {
|
||||
const link = await prisma.platformLink.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
if (link) return link;
|
||||
|
||||
if (HardwarePlatform[id as HardwarePlatform]) {
|
||||
return await prisma.platformLink.create({
|
||||
data: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const userPlatform = await prisma.userPlatform.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!userPlatform) return undefined;
|
||||
return await prisma.platformLink.create({
|
||||
data: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -32,7 +32,7 @@ class SaveManager {
|
||||
},
|
||||
});
|
||||
if (!save)
|
||||
throw createError({ statusCode: 404, statusMessage: "Save not found" });
|
||||
throw createError({ statusCode: 404, message: "Save not found" });
|
||||
|
||||
const newSaveObjectId = randomUUID();
|
||||
const newSaveStream = await objectHandler.createWithStream(
|
||||
@ -43,7 +43,7 @@ class SaveManager {
|
||||
if (!newSaveStream)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to create writing stream to storage backend.",
|
||||
message: "Failed to create writing stream to storage backend.",
|
||||
});
|
||||
|
||||
let hash: string | undefined;
|
||||
@ -64,7 +64,7 @@ class SaveManager {
|
||||
await objectHandler.deleteAsSystem(newSaveObjectId);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Hash failed to generate",
|
||||
message: "Hash failed to generate",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -80,7 +80,7 @@ class ScreenshotManager {
|
||||
if (!saveStream)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to create writing stream to storage backend.",
|
||||
message: "Failed to create writing stream to storage backend.",
|
||||
});
|
||||
|
||||
// pipe into object store
|
||||
|
||||
@ -2,7 +2,7 @@ import type { H3Event } from "h3";
|
||||
import type { Session, SessionProvider } from "./types";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { parse as parseCookies } from "cookie-es";
|
||||
import type { MinimumRequestObject } from "~/server/h3";
|
||||
import type { MinimumRequestObject } from "~~/server/h3";
|
||||
import type { DurationLike } from "luxon";
|
||||
import { DateTime } from "luxon";
|
||||
import createDBSessionHandler from "./db";
|
||||
|
||||
@ -1,22 +1,32 @@
|
||||
export const taskGroups = {
|
||||
"cleanup:invitations": {
|
||||
concurrency: false,
|
||||
},
|
||||
"cleanup:objects": {
|
||||
concurrency: false,
|
||||
},
|
||||
"cleanup:sessions": {
|
||||
concurrency: false,
|
||||
},
|
||||
"check:update": {
|
||||
concurrency: false,
|
||||
},
|
||||
"import:game": {
|
||||
concurrency: true,
|
||||
},
|
||||
debug: {
|
||||
concurrency: true,
|
||||
},
|
||||
} as const;
|
||||
export const TASK_GROUPS = [
|
||||
"cleanup:invitations",
|
||||
"cleanup:objects",
|
||||
"cleanup:sessions",
|
||||
"check:update",
|
||||
"import:game",
|
||||
"import:version",
|
||||
] as const;
|
||||
|
||||
export type TaskGroup = keyof typeof taskGroups;
|
||||
export type TaskGroup = (typeof TASK_GROUPS)[number];
|
||||
|
||||
export const TASK_GROUP_CONFIG: { [key in TaskGroup]: { concurrency: boolean } } =
|
||||
{
|
||||
"cleanup:invitations": {
|
||||
concurrency: false
|
||||
},
|
||||
"cleanup:objects": {
|
||||
concurrency: false
|
||||
},
|
||||
"cleanup:sessions": {
|
||||
concurrency: false
|
||||
},
|
||||
"check:update": {
|
||||
concurrency: false
|
||||
},
|
||||
"import:game": {
|
||||
concurrency: true
|
||||
},
|
||||
"import:version": {
|
||||
concurrency: true
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import droplet from "@drop-oss/droplet";
|
||||
import type { MinimumRequestObject } from "~/server/h3";
|
||||
import type { MinimumRequestObject } from "~~/server/h3";
|
||||
import type { GlobalACL } from "../acls";
|
||||
import aclManager from "../acls";
|
||||
|
||||
@ -7,12 +7,13 @@ import cleanupInvites from "./registry/invitations";
|
||||
import cleanupSessions from "./registry/sessions";
|
||||
import checkUpdate from "./registry/update";
|
||||
import cleanupObjects from "./registry/objects";
|
||||
import { taskGroups, type TaskGroup } from "./group";
|
||||
import { TASK_GROUP_CONFIG, type TaskGroup } from "./group";
|
||||
import prisma from "../db/database";
|
||||
import { type } from "arktype";
|
||||
import pino from "pino";
|
||||
import { logger } from "~/server/internal/logging";
|
||||
import { logger } from "~~/server/internal/logging";
|
||||
import { Writable } from "node:stream";
|
||||
import type { TaskRunContext } from "./utils";
|
||||
|
||||
// a task that has been run
|
||||
type FinishedTask = {
|
||||
@ -53,7 +54,6 @@ class TaskHandler {
|
||||
"cleanup:invitations",
|
||||
"cleanup:sessions",
|
||||
"check:update",
|
||||
"debug",
|
||||
];
|
||||
private weeklyScheduledTasks: TaskGroup[] = ["cleanup:objects"];
|
||||
|
||||
@ -82,7 +82,7 @@ class TaskHandler {
|
||||
let logOffset: number = 0;
|
||||
|
||||
// if taskgroup disallows concurrency
|
||||
if (!taskGroups[task.taskGroup].concurrency) {
|
||||
if (!TASK_GROUP_CONFIG[task.taskGroup].concurrency) {
|
||||
for (const existingTask of this.taskPool.values()) {
|
||||
// if a task is already running, we don't want to start another
|
||||
if (existingTask.taskGroup === task.taskGroup) {
|
||||
@ -149,7 +149,7 @@ class TaskHandler {
|
||||
}
|
||||
} catch (e) {
|
||||
// fallback: ignore or log error
|
||||
logger.error("Failed to parse log chunk", {
|
||||
logger.error("Failed to parse log chunk %s", {
|
||||
error: e,
|
||||
chunk: chunk,
|
||||
});
|
||||
@ -177,7 +177,7 @@ class TaskHandler {
|
||||
|
||||
const progress = (progress: number) => {
|
||||
if (progress < 0 || progress > 100) {
|
||||
logger.error("Progress must be between 0 and 100", { progress });
|
||||
logger.error("Progress must be between 0 and 100, actually %d", progress);
|
||||
return;
|
||||
}
|
||||
const taskEntry = this.taskPool.get(task.id);
|
||||
@ -412,36 +412,6 @@ class TaskHandler {
|
||||
}
|
||||
}
|
||||
|
||||
export type TaskRunContext = {
|
||||
progress: (progress: number) => void;
|
||||
logger: typeof logger;
|
||||
};
|
||||
|
||||
export function wrapTaskContext(
|
||||
context: TaskRunContext,
|
||||
options: { min: number; max: number; prefix: string },
|
||||
): TaskRunContext {
|
||||
const child = context.logger.child({
|
||||
prefix: options.prefix,
|
||||
});
|
||||
|
||||
return {
|
||||
progress(progress) {
|
||||
if (progress > 100 || progress < 0) {
|
||||
logger.warn("[wrapTaskContext] progress must be between 0 and 100");
|
||||
}
|
||||
|
||||
// I was too tired to figure this out
|
||||
// https://stackoverflow.com/a/929107
|
||||
const oldRange = 100;
|
||||
const newRange = options.max - options.min;
|
||||
const adjustedProgress = (progress * newRange) / oldRange + options.min;
|
||||
return context.progress(adjustedProgress);
|
||||
},
|
||||
logger: child,
|
||||
};
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
taskGroup: TaskGroup;
|
||||
@ -484,31 +454,6 @@ export const TaskLog = type({
|
||||
level: "string",
|
||||
});
|
||||
|
||||
// /**
|
||||
// * Create a log message with a timestamp in the format YYYY-MM-DD HH:mm:ss.SSS UTC
|
||||
// * @param message
|
||||
// * @returns
|
||||
// */
|
||||
// function msgWithTimestamp(message: string): string {
|
||||
// const now = new Date();
|
||||
|
||||
// const pad = (n: number, width = 2) => n.toString().padStart(width, "0");
|
||||
|
||||
// const year = now.getUTCFullYear();
|
||||
// const month = pad(now.getUTCMonth() + 1);
|
||||
// const day = pad(now.getUTCDate());
|
||||
|
||||
// const hours = pad(now.getUTCHours());
|
||||
// const minutes = pad(now.getUTCMinutes());
|
||||
// const seconds = pad(now.getUTCSeconds());
|
||||
// const milliseconds = pad(now.getUTCMilliseconds(), 3);
|
||||
|
||||
// const log: typeof TaskLog.infer = {
|
||||
// timestamp: `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds} UTC`,
|
||||
// message,
|
||||
// };
|
||||
// return JSON.stringify(log);
|
||||
// }
|
||||
|
||||
export function defineDropTask(buildTask: BuildTask): DropTask {
|
||||
return {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { defineDropTask } from "..";
|
||||
// import { defineDropTask } from "..";
|
||||
|
||||
/*
|
||||
export default defineDropTask({
|
||||
buildId: () => `debug:${new Date().toISOString()}`,
|
||||
name: "Debug Task",
|
||||
@ -16,3 +17,4 @@ export default defineDropTask({
|
||||
}
|
||||
},
|
||||
});
|
||||
*/
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import prisma from "~~/server/internal/db/database";
|
||||
import { defineDropTask } from "..";
|
||||
|
||||
export default defineDropTask({
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import prisma from "~/server/internal/db/database";
|
||||
import objectHandler from "~/server/internal/objects";
|
||||
import prisma from "~~/server/internal/db/database";
|
||||
import objectHandler from "~~/server/internal/objects";
|
||||
import { defineDropTask } from "..";
|
||||
|
||||
type FieldReferenceMap = {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import sessionHandler from "~/server/internal/session";
|
||||
import sessionHandler from "~~/server/internal/session";
|
||||
import { defineDropTask } from "..";
|
||||
|
||||
export default defineDropTask({
|
||||
|
||||
@ -49,7 +49,7 @@ export default defineDropTask({
|
||||
|
||||
// if response failed somehow
|
||||
if (!response.ok) {
|
||||
logger.info("Failed to check for update ", {
|
||||
logger.info("Failed to check for update: %s", {
|
||||
status: response.status,
|
||||
body: response.body,
|
||||
});
|
||||
|
||||
58
server/internal/tasks/utils.ts
Normal file
58
server/internal/tasks/utils.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { logger } from "../logging";
|
||||
|
||||
export type TaskRunContext = {
|
||||
progress: (progress: number) => void;
|
||||
logger: typeof logger;
|
||||
};
|
||||
|
||||
export function wrapTaskContext(
|
||||
context: TaskRunContext,
|
||||
options: { min: number; max: number; prefix: string },
|
||||
): TaskRunContext {
|
||||
const child = context.logger.child({
|
||||
prefix: options.prefix,
|
||||
});
|
||||
|
||||
return {
|
||||
progress(progress) {
|
||||
if (progress > 100 || progress < 0) {
|
||||
logger.warn("[wrapTaskContext] progress must be between 0 and 100");
|
||||
}
|
||||
|
||||
// I was too tired to figure this out
|
||||
// https://stackoverflow.com/a/929107
|
||||
const oldRange = 100;
|
||||
const newRange = options.max - options.min;
|
||||
const adjustedProgress = (progress * newRange) / oldRange + options.min;
|
||||
return context.progress(adjustedProgress);
|
||||
},
|
||||
logger: child,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// /**
|
||||
// * Create a log message with a timestamp in the format YYYY-MM-DD HH:mm:ss.SSS UTC
|
||||
// * @param message
|
||||
// * @returns
|
||||
// */
|
||||
// function msgWithTimestamp(message: string): string {
|
||||
// const now = new Date();
|
||||
|
||||
// const pad = (n: number, width = 2) => n.toString().padStart(width, "0");
|
||||
|
||||
// const year = now.getUTCFullYear();
|
||||
// const month = pad(now.getUTCMonth() + 1);
|
||||
// const day = pad(now.getUTCDate());
|
||||
|
||||
// const hours = pad(now.getUTCHours());
|
||||
// const minutes = pad(now.getUTCMinutes());
|
||||
// const seconds = pad(now.getUTCSeconds());
|
||||
// const milliseconds = pad(now.getUTCMilliseconds(), 3);
|
||||
|
||||
// const log: typeof TaskLog.infer = {
|
||||
// timestamp: `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds} UTC`,
|
||||
// message,
|
||||
// };
|
||||
// return JSON.stringify(log);
|
||||
// }
|
||||
@ -1,18 +1,27 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type { EventHandlerRequest, H3Event } from "h3";
|
||||
import type { Dump, Pull } from "../objects/transactional";
|
||||
import type { Dump, Pull, Register } from "../objects/transactional";
|
||||
import { ObjectTransactionalHandler } from "../objects/transactional";
|
||||
|
||||
type RecursiveType =
|
||||
| { [key: string]: RecursiveType }
|
||||
| string
|
||||
| number
|
||||
| Array<RecursiveType>;
|
||||
|
||||
export async function handleFileUpload(
|
||||
h3: H3Event<EventHandlerRequest>,
|
||||
metadata: { [key: string]: string },
|
||||
permissions: Array<string>,
|
||||
max = -1,
|
||||
): Promise<[string[], { [key: string]: string }, Pull, Dump] | undefined> {
|
||||
): Promise<
|
||||
[string[], { [key: string]: RecursiveType }, Pull, Dump, Register] | undefined
|
||||
> {
|
||||
const formData = await readMultipartFormData(h3);
|
||||
if (!formData) return undefined;
|
||||
const transactionalHandler = new ObjectTransactionalHandler();
|
||||
const [add, pull, dump] = transactionalHandler.new(metadata, permissions);
|
||||
const options: { [key: string]: string } = {};
|
||||
const options: any = {};
|
||||
const ids = [];
|
||||
|
||||
for (const entry of formData) {
|
||||
@ -25,8 +34,15 @@ export async function handleFileUpload(
|
||||
}
|
||||
if (!entry.name) continue;
|
||||
|
||||
options[entry.name] = entry.data.toString("utf-8");
|
||||
const path = entry.name.split(".");
|
||||
let v = options;
|
||||
for (const pathPart of path.slice(0, -1)) {
|
||||
(v as any)[pathPart] ??= {};
|
||||
v = (v as any)[pathPart];
|
||||
}
|
||||
|
||||
(v as any)[path.at(-1)!] = entry.data.toString("utf-8");
|
||||
}
|
||||
|
||||
return [ids, options, pull, dump];
|
||||
return [ids, options, pull, dump, add];
|
||||
}
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { Platform } from "~/prisma/client/enums";
|
||||
import { HardwarePlatform } from "~~/prisma/client/enums";
|
||||
|
||||
export function parsePlatform(platform: string) {
|
||||
switch (platform.toLowerCase()) {
|
||||
case "linux":
|
||||
return Platform.Linux;
|
||||
return HardwarePlatform.Linux;
|
||||
case "windows":
|
||||
return Platform.Windows;
|
||||
return HardwarePlatform.Windows;
|
||||
case "mac":
|
||||
case "macos":
|
||||
return Platform.macOS;
|
||||
return HardwarePlatform.macOS;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@ -80,11 +80,12 @@ export class PriorityListIndexed<T> extends PriorityList<T> {
|
||||
|
||||
override pop(position?: number): PriorityTagged<T> {
|
||||
const value = super.pop(position);
|
||||
if(!value) return undefined!;
|
||||
|
||||
const index = this.getIndex(value.object);
|
||||
this.indexMap.delete(index);
|
||||
|
||||
return value;
|
||||
return value!;
|
||||
}
|
||||
|
||||
get(index: string) {
|
||||
|
||||
Reference in New Issue
Block a user