13 Commits

Author SHA1 Message Date
973c3efa18 Merge branch 'develop' into small-fixes 2025-11-21 23:21:11 +11:00
bcb88f8069 fix: type errors 2025-11-21 23:20:53 +11:00
b842d78b94 fix: oidc scopes override 2025-11-21 23:18:24 +11:00
b0bf1a2795 fix: bump droplet 2025-11-21 23:08:49 +11:00
2d165bf997 fix: typescript for lint 2025-11-21 23:07:50 +11:00
650a3ca98d fix: add no-prisma-delete lint 2025-11-21 23:04:00 +11:00
246c97ccc9 Add additional content screenshots for Steam provider (#284) 2025-11-21 22:27:36 +11:00
f1fccd9bff Remove .gitlab-ci.yml 2025-11-20 16:09:16 +11:00
2ae7f41be0 Fix 7z archives with spaces (#288)
* fix: ignore imported versions

* fix: bump droplet for 7z fixes
2025-11-20 14:02:56 +11:00
beb824c8d9 Add metadata timeout (#287)
* Add metadata timeout

* Fix lint
2025-11-20 11:17:58 +11:00
8f41024be2 Fix Prisma build 2025-11-15 10:59:17 +11:00
2420814862 Add 7zip to container 2025-11-15 10:01:58 +11:00
41855bccd2 Bump version 2025-11-15 09:05:14 +11:00
38 changed files with 3031 additions and 1003 deletions

View File

@ -1,54 +0,0 @@
variables:
GIT_SUBMODULE_STRATEGY: recursive
stages:
- build
services:
- docker:24.0.5-dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
build:
stage: build
image: docker:latest
variables:
IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHORT_SHA
LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:latest
PUBLISH_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
PUBLISH_LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest
script:
- docker build -t $IMAGE_NAME .
- docker image tag $IMAGE_NAME $LATEST_IMAGE_NAME
- docker push $IMAGE_NAME
- docker push $LATEST_IMAGE_NAME
- |
if [ $CI_COMMIT_TAG ]; then
docker image tag $IMAGE_NAME $PUBLISH_IMAGE_NAME
docker image tag $IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME
docker push $PUBLISH_IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME
fi
build-arm64:
stage: build
image: arm64v8/docker:latest
tags:
- aarch64
variables:
IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHORT_SHA-arm64
LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:latest-arm64
PUBLISH_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG-arm64
PUBLISH_LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest-arm64
script:
- docker build -t $IMAGE_NAME . --platform=linux/arm64
- docker image tag $IMAGE_NAME $LATEST_IMAGE_NAME
- docker push $IMAGE_NAME
- docker push $LATEST_IMAGE_NAME
- |
if [ $CI_COMMIT_TAG ]; then
docker image tag $IMAGE_NAME $PUBLISH_IMAGE_NAME
docker image tag $IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME
docker push $PUBLISH_IMAGE_NAME
docker push $PUBLISH_LATEST_IMAGE_NAME
fi

View File

@ -45,12 +45,12 @@ ENV NODE_ENV=production
ENV NUXT_TELEMETRY_DISABLED=1 ENV NUXT_TELEMETRY_DISABLED=1
# RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn add --network-timeout 1000000 --no-lockfile --ignore-scripts prisma@6.11.1 # RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn add --network-timeout 1000000 --no-lockfile --ignore-scripts prisma@6.11.1
RUN apk add --no-cache pnpm RUN apk add --no-cache pnpm 7zip
RUN pnpm install prisma@6.11.1 RUN pnpm install prisma@6.11.1
# init prisma to download all required files # init prisma to download all required files
RUN pnpm prisma init RUN pnpm prisma init
COPY --from=build-system /app/package.json ./ COPY --from=build-system /app/prisma.config.ts ./
COPY --from=build-system /app/.output ./app COPY --from=build-system /app/.output ./app
COPY --from=build-system /app/prisma ./prisma COPY --from=build-system /app/prisma ./prisma
COPY --from=build-system /app/build ./startup COPY --from=build-system /app/build ./startup

View File

@ -1,4 +1,3 @@
import type { RouteLocationNormalized } from "vue-router";
import type { NavigationItem } from "./types"; import type { NavigationItem } from "./types";
export const useCurrentNavigationIndex = ( export const useCurrentNavigationIndex = (
@ -9,7 +8,7 @@ export const useCurrentNavigationIndex = (
const currentNavigation = ref(-1); const currentNavigation = ref(-1);
function calculateCurrentNavIndex(to: RouteLocationNormalized) { function calculateCurrentNavIndex(to: typeof route) {
const validOptions = navigation const validOptions = navigation
.map((e, i) => ({ ...e, index: i })) .map((e, i) => ({ ...e, index: i }))
.filter((e) => to.fullPath.startsWith(e.prefix)); .filter((e) => to.fullPath.startsWith(e.prefix));

View File

@ -2,6 +2,7 @@
import withNuxt from "./.nuxt/eslint.config.mjs"; import withNuxt from "./.nuxt/eslint.config.mjs";
import eslintConfigPrettier from "eslint-config-prettier/flat"; import eslintConfigPrettier from "eslint-config-prettier/flat";
import vueI18n from "@intlify/eslint-plugin-vue-i18n"; import vueI18n from "@intlify/eslint-plugin-vue-i18n";
import noPrismaDelete from "./rules/no-prisma-delete.mts";
export default withNuxt([ export default withNuxt([
eslintConfigPrettier, eslintConfigPrettier,
@ -19,6 +20,7 @@ export default withNuxt([
}, },
], ],
"@intlify/vue-i18n/no-missing-keys": "error", "@intlify/vue-i18n/no-missing-keys": "error",
"drop/no-prisma-delete": "error",
}, },
settings: { settings: {
"vue-i18n": { "vue-i18n": {
@ -29,5 +31,8 @@ export default withNuxt([
messageSyntaxVersion: "^11.0.0", messageSyntaxVersion: "^11.0.0",
}, },
}, },
plugins: {
drop: { rules: { "no-prisma-delete": noPrismaDelete } },
},
}, },
]); ]);

View File

@ -1,10 +1,10 @@
<template> <template>
<div v-if="!noWrapper" class="flex flex-col w-full min-h-screen bg-zinc-900"> <div v-if="!noWrapper" class="flex flex-col w-full min-h-screen bg-zinc-900">
<UserHeader class="z-50" hydrate-on-idle /> <LazyUserHeader class="z-50" hydrate-on-idle />
<div class="grow flex"> <div class="grow flex">
<NuxtPage /> <NuxtPage />
</div> </div>
<UserFooter class="z-50" hydrate-on-interaction /> <LazyUserFooter class="z-50" hydrate-on-interaction />
</div> </div>
<div v-else class="flex w-full min-h-screen bg-zinc-900"> <div v-else class="flex w-full min-h-screen bg-zinc-900">
<NuxtPage /> <NuxtPage />

View File

@ -175,6 +175,9 @@ export default defineNuxtConfig({
}, },
i18n: { i18n: {
bundle: {
optimizeTranslationDirective: false,
},
defaultLocale: "en-us", defaultLocale: "en-us",
strategy: "no_prefix", strategy: "no_prefix",
experimental: { experimental: {

View File

@ -1,6 +1,6 @@
{ {
"name": "drop", "name": "drop",
"version": "0.3.3", "version": "0.3.4",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
@ -21,7 +21,7 @@
}, },
"dependencies": { "dependencies": {
"@discordapp/twemoji": "^16.0.1", "@discordapp/twemoji": "^16.0.1",
"@drop-oss/droplet": "3.2.0", "@drop-oss/droplet": "3.5.0",
"@headlessui/vue": "^1.7.23", "@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5", "@heroicons/vue": "^2.1.5",
"@lobomfz/prismark": "0.0.3", "@lobomfz/prismark": "0.0.3",
@ -37,6 +37,7 @@
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"cheerio": "^1.0.0", "cheerio": "^1.0.0",
"cookie-es": "^2.0.0", "cookie-es": "^2.0.0",
"dotenv": "^17.2.3",
"fast-fuzzy": "^1.12.0", "fast-fuzzy": "^1.12.0",
"file-type-mime": "^0.4.3", "file-type-mime": "^0.4.3",
"jdenticon": "^3.3.0", "jdenticon": "^3.3.0",
@ -47,7 +48,7 @@
"nuxt-security": "2.2.0", "nuxt-security": "2.2.0",
"pino": "^9.7.0", "pino": "^9.7.0",
"pino-pretty": "^13.0.0", "pino-pretty": "^13.0.0",
"prisma": "^6.11.1", "prisma": "6.11.1",
"sanitize-filename": "^1.6.3", "sanitize-filename": "^1.6.3",
"semver": "^7.7.1", "semver": "^7.7.1",
"stream-mime-type": "^2.0.0", "stream-mime-type": "^2.0.0",
@ -65,7 +66,6 @@
"@nuxt/eslint": "^1.3.0", "@nuxt/eslint": "^1.3.0",
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.15",
"@types/bcryptjs": "^3.0.0",
"@types/luxon": "^3.6.2", "@types/luxon": "^3.6.2",
"@types/node": "^22.13.16", "@types/node": "^22.13.16",
"@types/semver": "^7.7.0", "@types/semver": "^7.7.0",
@ -87,8 +87,5 @@
"vue3-carousel": "^0.16.0" "vue3-carousel": "^0.16.0"
} }
}, },
"prisma": {
"schema": "./prisma"
},
"packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b" "packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b"
} }

3691
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,10 @@
onlyBuiltDependencies:
- "@prisma/client"
- "@prisma/engines"
- "@tailwindcss/oxide"
- esbuild
- prisma
overrides: overrides:
droplet: link:../../.local/share/pnpm/global/5/node_modules/@drop-oss/droplet droplet: link:../../.local/share/pnpm/global/5/node_modules/@drop-oss/droplet

10
prisma.config.ts Normal file
View File

@ -0,0 +1,10 @@
import { config } from "dotenv";
import type { PrismaConfig } from "prisma";
import path from "node:path";
config();
export default {
schema: path.join("prisma"),
earlyAccess: true,
} satisfies PrismaConfig;

View File

@ -0,0 +1,34 @@
import type { TSESLint } from "@typescript-eslint/utils";
export default {
meta: {
type: "problem",
docs: {
description: "Don't use Prisma error-prone .delete function",
},
messages: {
noPrismaDelete:
"Prisma .delete(...) function is used. Use .deleteMany(..) and check count instead.",
},
schema: [],
},
create(context) {
return {
CallExpression: function (node) {
// @ts-expect-error It ain't typing properly
const funcId = node.callee.property;
if (!funcId || funcId.name !== "delete") return;
// @ts-expect-error It ain't typing properly
const tableExpr = node.callee.object;
if (!tableExpr) return;
const prismaExpr = tableExpr.object;
if (!prismaExpr || prismaExpr.name !== "prisma") return;
context.report({
node,
messageId: "noPrismaDelete",
});
},
};
},
defaultOptions: [],
} satisfies TSESLint.RuleModule<"noPrismaDelete">;

View File

@ -17,6 +17,10 @@ export default defineEventHandler<{
const body = await readDropValidatedBody(h3, DeleteInvite); const body = await readDropValidatedBody(h3, DeleteInvite);
await prisma.invitation.delete({ where: { id: body.id } }); const { count } = await prisma.invitation.deleteMany({
where: { id: body.id },
});
if (count == 0)
throw createError({ statusCode: 404, message: "Invitation not found." });
return {}; return {};
}); });

View File

@ -7,7 +7,7 @@ export default defineEventHandler(async (h3) => {
const gameId = getRouterParam(h3, "id")!; const gameId = getRouterParam(h3, "id")!;
libraryManager.deleteGame(gameId); await libraryManager.deleteGame(gameId);
return {}; return {};
}); });

View File

@ -18,11 +18,13 @@ export default defineEventHandler<{ body: typeof DeleteLibrarySource.infer }>(
const body = await readDropValidatedBody(h3, DeleteLibrarySource); const body = await readDropValidatedBody(h3, DeleteLibrarySource);
await prisma.library.delete({ const { count } = await prisma.library.deleteMany({
where: { where: {
id: body.id, id: body.id,
}, },
}); });
if (count == 0)
throw createError({ statusCode: 404, message: "Library not found." });
libraryManager.removeLibrary(body.id); libraryManager.removeLibrary(body.id);
}, },

View File

@ -13,10 +13,10 @@ export default defineEventHandler(async (h3) => {
statusMessage: "No id in router params", statusMessage: "No id in router params",
}); });
const deleted = await prisma.aPIToken.delete({ const { count } = await prisma.aPIToken.deleteMany({
where: { id: id, mode: APITokenMode.System }, where: { id: id, mode: APITokenMode.System },
})!; })!;
if (!deleted) if (count == 0)
throw createError({ statusCode: 404, statusMessage: "Token not found" }); throw createError({ statusCode: 404, statusMessage: "Token not found" });
return; return;

View File

@ -27,6 +27,7 @@ export default defineEventHandler(async (h3) => {
if (!user) if (!user)
throw createError({ statusCode: 404, statusMessage: "User not found." }); throw createError({ statusCode: 404, statusMessage: "User not found." });
// eslint-disable-next-line drop/no-prisma-delete
await prisma.user.delete({ where: { id: userId } }); await prisma.user.delete({ where: { id: userId } });
await userStatsManager.deleteUser(); await userStatsManager.deleteUser();
return { success: true }; return { success: true };

View File

@ -84,7 +84,7 @@ export default defineEventHandler<{
user: true, user: true,
}, },
}), }),
prisma.invitation.delete({ where: { id: user.invitation } }), prisma.invitation.deleteMany({ where: { id: user.invitation } }),
]); ]);
await userStatsManager.addUser(); await userStatsManager.addUser();

View File

@ -42,7 +42,7 @@ export default defineEventHandler(async (h3) => {
) )
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: "Invalid capabilities.", message: "Invalid capabilities.",
}); });
if ( if (

View File

@ -17,21 +17,10 @@ export default defineClientEventHandler(async (h3) => {
orderBy: { orderBy: {
versionIndex: "desc", // Latest one first versionIndex: "desc", // Latest one first
}, },
omit: {
dropletManifest: true,
},
}); });
const mappedVersions = versions return versions;
.map((version) => {
if (!version.dropletManifest) return undefined;
const newVersion = { ...version, dropletManifest: undefined };
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore idk why we delete an undefined object
delete newVersion.dropletManifest;
return {
...newVersion,
};
})
.filter((e) => e);
return mappedVersions;
}); });

View File

@ -38,16 +38,14 @@ export default defineClientEventHandler(
if (!game) if (!game)
throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); throw createError({ statusCode: 400, statusMessage: "Invalid game ID" });
const save = await prisma.saveSlot.delete({ const { count } = await prisma.saveSlot.deleteMany({
where: { where: {
id: { userId: user.id,
userId: user.id, gameId: gameId,
gameId: gameId, index: slotIndex,
index: slotIndex,
},
}, },
}); });
if (!save) if (count == 0)
throw createError({ statusCode: 404, statusMessage: "Save not found" }); throw createError({ statusCode: 404, statusMessage: "Save not found" });
}, },
); );

View File

@ -20,14 +20,14 @@ export default defineEventHandler(async (h3) => {
userIds.push("system"); userIds.push("system");
} }
const notification = await prisma.notification.delete({ const { count } = await prisma.notification.deleteMany({
where: { where: {
id: notificationId, id: notificationId,
userId: { in: userIds }, userId: { in: userIds },
}, },
}); });
if (!notification) if (count == 0)
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: "Invalid notification ID", statusMessage: "Invalid notification ID",

View File

@ -13,10 +13,10 @@ export default defineEventHandler(async (h3) => {
statusMessage: "No id in router params", statusMessage: "No id in router params",
}); });
const deleted = await prisma.aPIToken.delete({ const { count } = await prisma.aPIToken.deleteMany({
where: { id: id, userId: userId, mode: APITokenMode.User }, where: { id: id, userId: userId, mode: APITokenMode.User },
})!; })!;
if (!deleted) if (count == 0)
throw createError({ statusCode: 404, statusMessage: "Token not found" }); throw createError({ statusCode: 404, statusMessage: "Token not found" });
return; return;

View File

@ -66,6 +66,7 @@ export class OIDCManager {
async create() { async create() {
const wellKnownUrl = process.env.OIDC_WELLKNOWN as string | undefined; const wellKnownUrl = process.env.OIDC_WELLKNOWN as string | undefined;
const scopes = process.env.OIDC_SCOPES as string | undefined;
let configuration: OIDCWellKnown; let configuration: OIDCWellKnown;
if (wellKnownUrl) { if (wellKnownUrl) {
const response: OIDCWellKnown = await $fetch<OIDCWellKnown>(wellKnownUrl); const response: OIDCWellKnown = await $fetch<OIDCWellKnown>(wellKnownUrl);
@ -77,6 +78,9 @@ export class OIDCManager {
) { ) {
throw new Error("Well known response was invalid"); throw new Error("Well known response was invalid");
} }
if (scopes) {
response.scopes_supported = scopes.split(",");
}
configuration = response; configuration = response;
} else { } else {
@ -85,7 +89,6 @@ export class OIDCManager {
| undefined; | undefined;
const tokenEndpoint = process.env.OIDC_TOKEN as string | undefined; const tokenEndpoint = process.env.OIDC_TOKEN as string | undefined;
const userinfoEndpoint = process.env.OIDC_USERINFO as string | undefined; const userinfoEndpoint = process.env.OIDC_USERINFO as string | undefined;
const scopes = process.env.OIDC_SCOPES as string | undefined;
if ( if (
!authorizationEndpoint || !authorizationEndpoint ||

View File

@ -185,15 +185,19 @@ export class ClientHandler {
} }
async removeClient(id: string) { async removeClient(id: string) {
const client = await prisma.client.findUnique({ where: { id } });
if (!client) return false;
const ca = useCertificateAuthority(); const ca = useCertificateAuthority();
await ca.blacklistClient(id); await ca.blacklistClient(id);
// eslint-disable-next-line drop/no-prisma-delete
await prisma.client.delete({ await prisma.client.delete({
where: { where: {
id, id,
}, },
}); });
await userStatsManager.cacheUserStats(); await userStatsManager.cacheUserStats();
return true;
} }
} }

View File

@ -4,6 +4,8 @@ class SystemConfig {
private libraryFolder = process.env.LIBRARY ?? "./.data/library"; private libraryFolder = process.env.LIBRARY ?? "./.data/library";
private dataFolder = process.env.DATA ?? "./.data/data"; private dataFolder = process.env.DATA ?? "./.data/data";
private metadataTimeout = parseInt(process.env.METADATA_TIMEOUT ?? "5000");
private externalUrl = normalizeUrl( private externalUrl = normalizeUrl(
process.env.EXTERNAL_URL ?? "http://localhost:3000", process.env.EXTERNAL_URL ?? "http://localhost:3000",
{ stripWWW: false }, { stripWWW: false },
@ -28,6 +30,10 @@ class SystemConfig {
return this.dataFolder; return this.dataFolder;
} }
getMetadataTimeout() {
return this.metadataTimeout;
}
getDropVersion() { getDropVersion() {
return this.dropVersion; return this.dropVersion;
} }

View File

@ -105,7 +105,10 @@ class LibraryManager {
if (!game) return undefined; if (!game) return undefined;
try { try {
const versions = await provider.listVersions(libraryPath); const versions = await provider.listVersions(
libraryPath,
game.versions.map((v) => v.versionName),
);
const unimportedVersions = versions.filter( const unimportedVersions = versions.filter(
(e) => (e) =>
game.versions.findIndex((v) => v.versionName == e) == -1 && game.versions.findIndex((v) => v.versionName == e) == -1 &&
@ -375,12 +378,10 @@ class LibraryManager {
} }
async deleteGameVersion(gameId: string, version: string) { async deleteGameVersion(gameId: string, version: string) {
await prisma.gameVersion.delete({ await prisma.gameVersion.deleteMany({
where: { where: {
gameId_versionName: { gameId: gameId,
gameId: gameId, versionName: version,
versionName: version,
},
}, },
}); });
@ -388,12 +389,12 @@ class LibraryManager {
} }
async deleteGame(gameId: string) { async deleteGame(gameId: string) {
await prisma.game.delete({ await prisma.game.deleteMany({
where: { where: {
id: gameId, id: gameId,
}, },
}); });
gameSizeManager.deleteGame(gameId); await gameSizeManager.deleteGame(gameId);
} }
async getGameVersionSize( async getGameVersionSize(

View File

@ -24,7 +24,10 @@ export abstract class LibraryProvider<CFG> {
* @param game folder name of the game to list versions for * @param game folder name of the game to list versions for
* @returns list of version folder names * @returns list of version folder names
*/ */
abstract listVersions(game: string): Promise<string[]>; abstract listVersions(
game: string,
existingPaths?: string[],
): Promise<string[]>;
/** /**
* @param game folder name of the game * @param game folder name of the game

View File

@ -54,11 +54,15 @@ export class FilesystemProvider
return folderDirs; return folderDirs;
} }
async listVersions(game: string): Promise<string[]> { async listVersions(
game: string,
ignoredVersions?: string[],
): Promise<string[]> {
const gameDir = path.join(this.config.baseDir, game); const gameDir = path.join(this.config.baseDir, game);
if (!fs.existsSync(gameDir)) throw new GameNotFoundError(); if (!fs.existsSync(gameDir)) throw new GameNotFoundError();
const versionDirs = fs.readdirSync(gameDir); const versionDirs = fs.readdirSync(gameDir);
const validVersionDirs = versionDirs.filter((e) => { const validVersionDirs = versionDirs.filter((e) => {
if (ignoredVersions && ignoredVersions.includes(e)) return false;
const fullDir = path.join(this.config.baseDir, game, e); const fullDir = path.join(this.config.baseDir, game, e);
return DROPLET_HANDLER.hasBackendForPath(fullDir); return DROPLET_HANDLER.hasBackendForPath(fullDir);
}); });
@ -109,17 +113,12 @@ export class FilesystemProvider
) { ) {
const filepath = path.join(this.config.baseDir, game, version); const filepath = path.join(this.config.baseDir, game, version);
if (!fs.existsSync(filepath)) return undefined; if (!fs.existsSync(filepath)) return undefined;
let stream; const stream = DROPLET_HANDLER.readFile(
while (!(stream instanceof ReadableStream)) { filepath,
const v = DROPLET_HANDLER.readFile( filename,
filepath, options?.start ? BigInt(options.start) : undefined,
filename, options?.end ? BigInt(options.end) : undefined,
options?.start ? BigInt(options.start) : undefined, );
options?.end ? BigInt(options.end) : undefined,
);
if (!v) return undefined;
stream = v.getStream() as ReadableStream<unknown>;
}
return stream; return stream;
} }

View File

@ -112,7 +112,7 @@ export class FlatFilesystemProvider
); );
if (!stream) return undefined; if (!stream) return undefined;
return stream.getStream(); return stream;
} }
fsStats() { fsStats() {

View File

@ -82,6 +82,10 @@ export class MetadataHandler {
// TODO: fix eslint error // TODO: fix eslint error
// eslint-disable-next-line no-async-promise-executor // eslint-disable-next-line no-async-promise-executor
>(async (resolve, reject) => { >(async (resolve, reject) => {
setTimeout(
() => reject(new Error("Timeout while fetching results")),
systemConfig.getMetadataTimeout(),
);
try { try {
const results = await provider.search(query); const results = await provider.search(query);
const mappedResults: InternalGameMetadataResult[] = results.map( const mappedResults: InternalGameMetadataResult[] = results.map(

View File

@ -117,6 +117,10 @@ interface SteamAppDetailsLarge extends SteamAppDetailsSmall {
filename: string; filename: string;
ordinal: number; ordinal: number;
}[]; }[];
mature_content_screenshots: {
filename: string;
ordinal: number;
}[];
}; };
full_description: string; full_description: string;
} }
@ -689,16 +693,20 @@ export class SteamProvider implements MetadataProvider {
context?.progress(40); context?.progress(40);
const images = [cover, banner]; const images = [cover, banner];
const screenshotCount = game.screenshots?.all_ages_screenshots?.length || 0;
context?.logger.info(`Processing ${screenshotCount} screenshots...`);
for (const image of game.screenshots?.all_ages_screenshots || []) { const screenshots = game.screenshots?.all_ages_screenshots || [];
screenshots.push(...(game.screenshots?.mature_content_screenshots || []));
screenshots.sort((a, b) => a.ordinal - b.ordinal);
context?.logger.info(`Processing ${screenshots.length} screenshots...`);
for (const image of screenshots) {
const imageUrl = this._getImageUrl(image.filename); const imageUrl = this._getImageUrl(image.filename);
images.push(createObject(imageUrl)); images.push(createObject(imageUrl));
} }
context?.logger.info( context?.logger.info(
`Image processing complete: icon, cover, banner and ${screenshotCount} screenshots`, `Image processing complete: icon, cover, banner and ${screenshots.length} screenshots`,
); );
context?.progress(50); context?.progress(50);

View File

@ -124,7 +124,10 @@ class NewsManager {
} }
async delete(id: string) { async delete(id: string) {
const article = await prisma.article.delete({ const article = await prisma.article.findUnique({ where: { id } });
if (!article) return false;
// eslint-disable-next-line drop/no-prisma-delete
await prisma.article.delete({
where: { id }, where: { id },
}); });
if (article.imageObjectId) { if (article.imageObjectId) {

View File

@ -259,16 +259,10 @@ class FsHashStore {
*/ */
async delete(id: ObjectReference) { async delete(id: ObjectReference) {
await this.cache.remove(id); await this.cache.remove(id);
await prisma.objectHash.deleteMany({
try { where: {
// need to catch in case the object doesn't exist id,
await prisma.objectHash.delete({ },
where: { });
id,
},
});
} catch {
/* empty */
}
} }
} }

View File

@ -53,12 +53,16 @@ class ScreenshotManager {
* @param id * @param id
*/ */
async delete(id: string) { async delete(id: string) {
const deletedScreenshot = await prisma.screenshot.delete({ const screenshot = await prisma.screenshot.findUnique({ where: { id } });
if (!screenshot) return false;
// eslint-disable-next-line drop/no-prisma-delete
await prisma.screenshot.delete({
where: { where: {
id, id,
}, },
}); });
await objectHandler.deleteAsSystem(deletedScreenshot.objectId); await objectHandler.deleteAsSystem(screenshot.objectId);
return true;
} }
/** /**

View File

@ -43,12 +43,12 @@ export default function createDBSessionHandler(): SessionProvider {
}, },
async removeSession(token) { async removeSession(token) {
await cache.remove(token); await cache.remove(token);
await prisma.session.delete({ const { count } = await prisma.session.deleteMany({
where: { where: {
token, token,
}, },
}); });
return true; return count > 0;
}, },
async cleanupSessions() { async cleanupSessions() {
const now = new Date(); const now = new Date();

View File

@ -101,19 +101,16 @@ class UserLibraryManager {
async collectionRemove(gameId: string, collectionId: string, userId: string) { async collectionRemove(gameId: string, collectionId: string, userId: string) {
// Delete if exists // Delete if exists
return ( const { count } = await prisma.collectionEntry.deleteMany({
( where: {
await prisma.collectionEntry.deleteMany({ collectionId,
where: { gameId,
collectionId, collection: {
gameId, userId,
collection: { },
userId, },
}, });
}, return count > 0;
})
).count > 0
);
} }
async collectionCreate(name: string, userId: string) { async collectionCreate(name: string, userId: string) {
@ -133,12 +130,13 @@ class UserLibraryManager {
} }
async deleteCollection(collectionId: string) { async deleteCollection(collectionId: string) {
await prisma.collection.delete({ const { count } = await prisma.collection.deleteMany({
where: { where: {
id: collectionId, id: collectionId,
isDefault: false, isDefault: false,
}, },
}); });
return count > 0;
} }
} }

View File

@ -1,5 +1,5 @@
import fs from "fs"; import fs from "node:fs";
import nodePath from "path"; import nodePath from "node:path";
export function fsStats(folderPath: string) { export function fsStats(folderPath: string) {
const stats = fs.statfsSync(folderPath); const stats = fs.statfsSync(folderPath);

View File

@ -2,6 +2,7 @@
// https://nuxt.com/docs/guide/concepts/typescript // https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json", "extends": "./.nuxt/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"exactOptionalPropertyTypes": false "exactOptionalPropertyTypes": false,
"allowImportingTsExtensions": true
} }
} }