diff --git a/components/GameSearchResultWidget.vue b/components/GameSearchResultWidget.vue new file mode 100644 index 0000000..b0dbce8 --- /dev/null +++ b/components/GameSearchResultWidget.vue @@ -0,0 +1,24 @@ + + + diff --git a/composables/task.ts b/composables/task.ts index e508b3a..6f7fcc1 100644 --- a/composables/task.ts +++ b/composables/task.ts @@ -20,7 +20,6 @@ function initWs() { ws = new WebSocket(url); ws.onmessage = (e) => { const msg = JSON.parse(e.data) as TaskMessage; - console.log(msg); const taskStates = useTaskStates(); const state = taskStates.value[msg.id]; if (!state) return; diff --git a/error.vue b/error.vue new file mode 100644 index 0000000..ce28c37 --- /dev/null +++ b/error.vue @@ -0,0 +1,95 @@ + + + diff --git a/layouts/admin.vue b/layouts/admin.vue index 5a08267..26459d9 100644 --- a/layouts/admin.vue +++ b/layouts/admin.vue @@ -49,11 +49,9 @@
-
-
- - -
+
+ +
@@ -71,13 +69,14 @@ import { } from "@heroicons/vue/24/outline"; import type { NavigationItem } from "~/composables/types"; import { useCurrentNavigationIndex } from "~/composables/current-page-engine"; +import { ArrowLeftIcon } from "@heroicons/vue/16/solid"; const navigation: Array = [ { label: "Home", route: "/admin", prefix: "/admin", icon: HomeIcon }, { - label: "Libraries", - route: "/admin/libraries", - prefix: "/admin/libraries", + label: "Library", + route: "/admin/library", + prefix: "/admin/library", icon: ServerStackIcon, }, { @@ -90,13 +89,19 @@ const navigation: Array = [ label: "Feature Flags", route: "/admin/features", prefix: "/admin/features", - icon: FlagIcon + icon: FlagIcon, }, { label: "Settings", route: "/admin/settings", prefix: "/admin/settings", - icon: Cog6ToothIcon + icon: Cog6ToothIcon, + }, + { + label: "Back", + route: "/", + prefix: ".", + icon: ArrowLeftIcon } ]; diff --git a/layouts/default.vue b/layouts/default.vue index 078a6b9..78a45f8 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -3,7 +3,6 @@
- {{ test }}
@@ -16,6 +15,4 @@ useHead({ return `Drop`; }, }); - -const test = useTask("test"); diff --git a/middleware/require-user.global.ts b/middleware/require-user.global.ts index 3739510..61286be 100644 --- a/middleware/require-user.global.ts +++ b/middleware/require-user.global.ts @@ -1,7 +1,10 @@ const whitelistedPrefixes = ["/signin", "/register", "/api"]; +const requireAdmin = ["/admin"]; export default defineNuxtRouteMiddleware(async (to, from) => { if (import.meta.server) return; + const error = useError(); + if (error.value !== undefined) return; if (whitelistedPrefixes.findIndex((e) => to.fullPath.startsWith(e)) != -1) return; @@ -12,4 +15,10 @@ export default defineNuxtRouteMiddleware(async (to, from) => { if (!user.value) { return navigateTo({ path: "/signin", query: { redirect: to.fullPath } }); } + if ( + requireAdmin.findIndex((e) => to.fullPath.startsWith(e)) != -1 && + !user.value.admin + ) { + return navigateTo({ path: "/" }); + } }); diff --git a/package.json b/package.json index d78ae2c..06c2976 100644 --- a/package.json +++ b/package.json @@ -10,15 +10,16 @@ "postinstall": "nuxt prepare" }, "dependencies": { - "@drop/droplet": "^0.3.2", + "@drop/droplet": "^0.4.1", "@headlessui/vue": "^1.7.23", "@heroicons/vue": "^2.1.5", "@prisma/client": "5.20.0", "axios": "^1.7.7", "bcrypt": "^5.1.1", + "fast-fuzzy": "^1.12.0", "file-type-mime": "^0.4.3", "moment": "^2.30.1", - "nuxt": "^3.13.0", + "nuxt": "^3.13.2", "prisma": "^5.20.0", "sanitize-filename": "^1.6.3", "stream": "^0.0.3", @@ -39,5 +40,9 @@ "postcss": "^8.4.47", "sass": "^1.79.4", "tailwindcss": "^3.4.13" + }, + "optionalDependencies": { + "@drop/droplet-linux-x64-gnu": "^0.4.1", + "@drop/droplet-win32-x64-msvc": "^0.4.1" } } diff --git a/pages/admin/libraries.vue b/pages/admin/libraries.vue deleted file mode 100644 index 5159503..0000000 --- a/pages/admin/libraries.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/pages/admin/library/[id]/import.vue b/pages/admin/library/[id]/import.vue new file mode 100644 index 0000000..1589256 --- /dev/null +++ b/pages/admin/library/[id]/import.vue @@ -0,0 +1,109 @@ + + + diff --git a/pages/admin/library/import.vue b/pages/admin/library/import.vue new file mode 100644 index 0000000..172b7ea --- /dev/null +++ b/pages/admin/library/import.vue @@ -0,0 +1,254 @@ + + + diff --git a/pages/admin/library/index.vue b/pages/admin/library/index.vue new file mode 100644 index 0000000..1d01e3b --- /dev/null +++ b/pages/admin/library/index.vue @@ -0,0 +1,122 @@ + + + diff --git a/pages/index.vue b/pages/index.vue index 48b5df7..57dfbd8 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -1,5 +1,5 @@ diff --git a/prisma/migrations/20241010062956_add_constraints/migration.sql b/prisma/migrations/20241010062956_add_constraints/migration.sql new file mode 100644 index 0000000..11e38bc --- /dev/null +++ b/prisma/migrations/20241010062956_add_constraints/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - A unique constraint covering the columns `[libraryBasePath]` on the table `Game` will be added. If there are existing duplicate values, this will fail. + - Added the required column `libraryBasePath` to the `Game` table without a default value. This is not possible if the table is not empty. + - Added the required column `versionOrder` to the `Game` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Game" ADD COLUMN "libraryBasePath" TEXT NOT NULL, +ADD COLUMN "versionOrder" TEXT NOT NULL; + +-- CreateTable +CREATE TABLE "GameVersion" ( + "gameId" TEXT NOT NULL, + "versionName" TEXT NOT NULL, + + CONSTRAINT "GameVersion_pkey" PRIMARY KEY ("gameId","versionName") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Game_libraryBasePath_key" ON "Game"("libraryBasePath"); + +-- AddForeignKey +ALTER TABLE "GameVersion" ADD CONSTRAINT "GameVersion_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20241010095344_various_fixes/migration.sql b/prisma/migrations/20241010095344_various_fixes/migration.sql new file mode 100644 index 0000000..030d2ee --- /dev/null +++ b/prisma/migrations/20241010095344_various_fixes/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - The `versionOrder` column on the `Game` table would be dropped and recreated. This will lead to data loss if there is data in the column. + - Changed the type of `platform` on the `Client` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + - Added the required column `launchCommand` to the `GameVersion` table without a default value. This is not possible if the table is not empty. + - Added the required column `platform` to the `GameVersion` table without a default value. This is not possible if the table is not empty. + - Added the required column `setupCommand` to the `GameVersion` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "Platform" AS ENUM ('windows', 'linux'); + +-- AlterTable +ALTER TABLE "Client" DROP COLUMN "platform", +ADD COLUMN "platform" "Platform" NOT NULL; + +-- AlterTable +ALTER TABLE "Game" DROP COLUMN "versionOrder", +ADD COLUMN "versionOrder" TEXT[]; + +-- AlterTable +ALTER TABLE "GameVersion" ADD COLUMN "launchCommand" TEXT NOT NULL, +ADD COLUMN "platform" "Platform" NOT NULL, +ADD COLUMN "setupCommand" TEXT NOT NULL; diff --git a/prisma/migrations/20241010104439_added_original_query_field/migration.sql b/prisma/migrations/20241010104439_added_original_query_field/migration.sql new file mode 100644 index 0000000..79aa26c --- /dev/null +++ b/prisma/migrations/20241010104439_added_original_query_field/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Added the required column `metadataOriginalQuery` to the `Developer` table without a default value. This is not possible if the table is not empty. + - Added the required column `metadataOriginalQuery` to the `Publisher` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Developer" ADD COLUMN "metadataOriginalQuery" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "Publisher" ADD COLUMN "metadataOriginalQuery" TEXT NOT NULL; diff --git a/prisma/migrations/20241010104722_fix_unique_constraints/migration.sql b/prisma/migrations/20241010104722_fix_unique_constraints/migration.sql new file mode 100644 index 0000000..6e22210 --- /dev/null +++ b/prisma/migrations/20241010104722_fix_unique_constraints/migration.sql @@ -0,0 +1,18 @@ +/* + Warnings: + + - A unique constraint covering the columns `[metadataSource,metadataId,metadataOriginalQuery]` on the table `Developer` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[metadataSource,metadataId,metadataOriginalQuery]` on the table `Publisher` will be added. If there are existing duplicate values, this will fail. + +*/ +-- DropIndex +DROP INDEX "Developer_metadataSource_metadataId_key"; + +-- DropIndex +DROP INDEX "Publisher_metadataSource_metadataId_key"; + +-- CreateIndex +CREATE UNIQUE INDEX "Developer_metadataSource_metadataId_metadataOriginalQuery_key" ON "Developer"("metadataSource", "metadataId", "metadataOriginalQuery"); + +-- CreateIndex +CREATE UNIQUE INDEX "Publisher_metadataSource_metadataId_metadataOriginalQuery_key" ON "Publisher"("metadataSource", "metadataId", "metadataOriginalQuery"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f091ed4..eae59f3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -45,6 +45,11 @@ enum ClientCapabilities { DownloadAggregation } +enum Platform { + Windows @map("windows") + Linux @map("linux") +} + // References a device model Client { id String @id @default(uuid()) @@ -55,7 +60,7 @@ model Client { capabilities ClientCapabilities[] name String - platform String + platform Platform lastConnected DateTime } @@ -86,9 +91,9 @@ model Game { mArt String[] // linked to objects in s3 mScreenshots String[] // linked to objects in s3 - versionOrder String + versionOrder String[] versions GameVersion[] - libraryBasePath String // Base dir for all the game versions + libraryBasePath String @unique // Base dir for all the game versions @@unique([metadataSource, metadataId], name: "metadataKey") } @@ -99,7 +104,9 @@ model GameVersion { game Game @relation(fields: [gameId], references: [id]) versionName String // Sub directory for the game files - + platform Platform + launchCommand String // Command to run to start. Platform-specific. Windows games on Linux will wrap this command in Proton/Wine + setupCommand String // Command to setup game (dependencies and such) @@id([gameId, versionName]) } @@ -107,8 +114,9 @@ model GameVersion { model Developer { id String @id @default(uuid()) - metadataSource MetadataSource - metadataId String + metadataSource MetadataSource + metadataId String + metadataOriginalQuery String mName String mShortDescription String @@ -119,14 +127,15 @@ model Developer { games Game[] - @@unique([metadataSource, metadataId], name: "metadataKey") + @@unique([metadataSource, metadataId, metadataOriginalQuery], name: "metadataKey") } model Publisher { id String @id @default(uuid()) - metadataSource MetadataSource - metadataId String + metadataSource MetadataSource + metadataId String + metadataOriginalQuery String mName String mShortDescription String @@ -137,5 +146,5 @@ model Publisher { games Game[] - @@unique([metadataSource, metadataId], name: "metadataKey") + @@unique([metadataSource, metadataId, metadataOriginalQuery], name: "metadataKey") } diff --git a/public/wallpapers/error-wallpaper.jpg b/public/wallpapers/error-wallpaper.jpg new file mode 100644 index 0000000..7d931ab Binary files /dev/null and b/public/wallpapers/error-wallpaper.jpg differ diff --git a/server/api/v1/admin/import/game/index.get.ts b/server/api/v1/admin/import/game/index.get.ts new file mode 100644 index 0000000..9137ada --- /dev/null +++ b/server/api/v1/admin/import/game/index.get.ts @@ -0,0 +1,9 @@ +import libraryManager from "~/server/internal/library"; + +export default defineEventHandler(async (h3) => { + const user = await h3.context.session.getAdminUser(h3); + if (!user) throw createError({ statusCode: 403 }); + + const unimportedGames = await libraryManager.fetchAllUnimportedGames(); + return { unimportedGames }; +}); diff --git a/server/api/v1/admin/import/game/index.post.ts b/server/api/v1/admin/import/game/index.post.ts new file mode 100644 index 0000000..43bb6ff --- /dev/null +++ b/server/api/v1/admin/import/game/index.post.ts @@ -0,0 +1,36 @@ +import libraryManager from "~/server/internal/library"; +import { + GameMetadataSearchResult, + GameMetadataSource, +} from "~/server/internal/metadata/types"; + +export default defineEventHandler(async (h3) => { + const user = await h3.context.session.getAdminUser(h3); + if (!user) throw createError({ statusCode: 403 }); + + const body = await readBody(h3); + + const path = body.path; + const metadata = body.metadata as GameMetadataSearchResult & + GameMetadataSource; + if (!path) + throw createError({ + statusCode: 400, + statusMessage: "Path missing from body", + }); + if (!metadata.id || !metadata.sourceId) + throw createError({ + statusCode: 400, + statusMessage: "Metadata IDs missing from body", + }); + + const validPath = await libraryManager.checkUnimportedGamePath(path); + if (!validPath) + throw createError({ + statusCode: 400, + statusMessage: "Invalid unimported game path", + }); + + const game = await h3.context.metadataHandler.createGame(metadata, path); + return game; +}); diff --git a/server/api/v1/admin/import/game/search.get.ts b/server/api/v1/admin/import/game/search.get.ts new file mode 100644 index 0000000..8cee3e2 --- /dev/null +++ b/server/api/v1/admin/import/game/search.get.ts @@ -0,0 +1,13 @@ +import libraryManager from "~/server/internal/library"; + +export default defineEventHandler(async (h3) => { + const user = await h3.context.session.getAdminUser(h3); + if (!user) throw createError({ statusCode: 403 }); + + const query = getQuery(h3); + const search = query.q?.toString(); + if (!search) + throw createError({ statusCode: 400, statusMessage: "Invalid search" }); + + return await h3.context.metadataHandler.search(search); +}); diff --git a/server/api/v1/admin/import/version/index.get.ts b/server/api/v1/admin/import/version/index.get.ts new file mode 100644 index 0000000..98251a4 --- /dev/null +++ b/server/api/v1/admin/import/version/index.get.ts @@ -0,0 +1,22 @@ +import libraryManager from "~/server/internal/library"; + +export default defineEventHandler(async (h3) => { + const user = await h3.context.session.getAdminUser(h3); + if (!user) throw createError({ statusCode: 403 }); + + const query = await getQuery(h3); + const gameId = query.id?.toString(); + if (!gameId) + throw createError({ + statusCode: 400, + statusMessage: "Missing id in request params", + }); + + const unimportedVersions = await libraryManager.fetchUnimportedVersions( + gameId + ); + if (!unimportedVersions) + throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); + + return unimportedVersions; +}); diff --git a/server/api/v1/admin/import/version/preload.get.ts b/server/api/v1/admin/import/version/preload.get.ts new file mode 100644 index 0000000..d14e0f5 --- /dev/null +++ b/server/api/v1/admin/import/version/preload.get.ts @@ -0,0 +1,27 @@ +import libraryManager from "~/server/internal/library"; + +export default defineEventHandler(async (h3) => { + const user = await h3.context.session.getAdminUser(h3); + if (!user) throw createError({ statusCode: 403 }); + + const query = await getQuery(h3); + const gameId = query.id?.toString(); + const versionName = query.version?.toString(); + if (!gameId || !versionName) + throw createError({ + statusCode: 400, + statusMessage: "Missing id or version in request params", + }); + + const preload = await libraryManager.fetchUnimportedVersionInformation( + gameId, + versionName + ); + if (!preload) + throw createError({ + statusCode: 400, + statusMessage: "Invalid game or version id/name", + }); + + return preload; +}); diff --git a/server/api/v1/admin/index.get.ts b/server/api/v1/admin/index.get.ts new file mode 100644 index 0000000..9cb0d05 --- /dev/null +++ b/server/api/v1/admin/index.get.ts @@ -0,0 +1,6 @@ +export default defineEventHandler(async (h3) => { + const user = await h3.context.session.getUser(h3); + if (!user) + throw createError({ statusCode: 403, statusMessage: "Not authenticated" }); + return { admin: user.admin }; +}); diff --git a/server/api/v1/admin/library/index.get.ts b/server/api/v1/admin/library/index.get.ts new file mode 100644 index 0000000..d526c1f --- /dev/null +++ b/server/api/v1/admin/library/index.get.ts @@ -0,0 +1,13 @@ +import libraryManager from "~/server/internal/library"; + +export default defineEventHandler(async (h3) => { + const user = await h3.context.session.getAdminUser(h3); + if (!user) throw createError({ statusCode: 403 }); + + const unimportedGames = await libraryManager.fetchAllUnimportedGames(); + const games = await libraryManager.fetchGamesWithStatus(); + + // Fetch other library data here + + return { unimportedGames, games }; +}); diff --git a/server/api/v1/auth/signup/simple.post.ts b/server/api/v1/auth/signup/simple.post.ts index cfbde1c..51530ed 100644 --- a/server/api/v1/auth/signup/simple.post.ts +++ b/server/api/v1/auth/signup/simple.post.ts @@ -24,7 +24,9 @@ export default defineEventHandler(async (h3) => { const userId = uuidv4(); - const profilePictureObject = await h3.context.objects.createFromSource( + const profilePictureId = uuidv4(); + await h3.context.objects.createFromSource( + profilePictureId, () => $fetch("https://avatars.githubusercontent.com/u/64579723?v=4", { responseType: "stream", @@ -32,18 +34,12 @@ export default defineEventHandler(async (h3) => { {}, [`anonymous:read`, `${userId}:write`] ); - if (!profilePictureObject) - throw createError({ - statusCode: 500, - statusMessage: "Unable to import profile picture", - }); - const user = await prisma.user.create({ data: { username, displayName: "DecDuck", email: "", - profilePicture: profilePictureObject, + profilePicture: profilePictureId, admin: true, }, }); diff --git a/server/api/v1/client/auth/initiate.post.ts b/server/api/v1/client/auth/initiate.post.ts index fbce624..39de29b 100644 --- a/server/api/v1/client/auth/initiate.post.ts +++ b/server/api/v1/client/auth/initiate.post.ts @@ -1,17 +1,25 @@ import clientHandler from "~/server/internal/clients/handler"; +import { parsePlatform } from "~/server/internal/utils/parseplatform"; export default defineEventHandler(async (h3) => { const body = await readBody(h3); const name = body.name; - const platform = body.platform; + const platformRaw = body.platform; - if (!name || !platform) + if (!name || !platformRaw) throw createError({ statusCode: 400, statusMessage: "Missing name or platform in body", }); + const platform = parsePlatform(platformRaw); + if (!platform) + throw createError({ + statusCode: 400, + statusMessage: "Invalid or unsupported platform", + }); + const clientId = await clientHandler.initiate({ name, platform }); return `/client/${clientId}/callback`; diff --git a/server/api/v1/games/front.get.ts b/server/api/v1/games/front.get.ts new file mode 100644 index 0000000..9a0e1f6 --- /dev/null +++ b/server/api/v1/games/front.get.ts @@ -0,0 +1,36 @@ +import prisma from "~/server/internal/db/database"; + +export default defineEventHandler(async (h3) => { + const userId = await h3.context.session.getUserId(h3); + if (!userId) throw createError({ statusCode: 403 }); + + const rawGames = await prisma.game.findMany({ + select: { + id: true, + mName: true, + mShortDescription: true, + mBannerId: true, + mDevelopers: { + select: { + id: true, + mName: true, + }, + }, + mPublishers: { + select: { + id: true, + mName: true, + }, + }, + versions: { + select: { + platform: true, + }, + }, + }, + }); + + const games = rawGames.map((e) => ({...e, platforms: e.versions.map((e) => e.platform).filter((e, _, r) => !r.includes(e))})) + + return games; +}); diff --git a/server/internal/clients/handler.ts b/server/internal/clients/handler.ts index aa64961..cf96d5f 100644 --- a/server/internal/clients/handler.ts +++ b/server/internal/clients/handler.ts @@ -1,10 +1,11 @@ import { v4 as uuidv4 } from "uuid"; import { CertificateBundle } from "./ca"; import prisma from "../db/database"; +import { Platform } from "@prisma/client"; export interface ClientMetadata { name: string; - platform: string; + platform: Platform; } export class ClientHandler { diff --git a/server/internal/db/database.ts b/server/internal/db/database.ts index 93ccb1d..595f449 100644 --- a/server/internal/db/database.ts +++ b/server/internal/db/database.ts @@ -10,6 +10,6 @@ declare const globalThis: { const prisma = globalThis.prismaGlobal ?? prismaClientSingleton() -export default prisma +export default prisma; if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma \ No newline at end of file diff --git a/server/internal/library/index.ts b/server/internal/library/index.ts new file mode 100644 index 0000000..00e5daa --- /dev/null +++ b/server/internal/library/index.ts @@ -0,0 +1,194 @@ +/** + * The Library Manager keeps track of games in Drop's library and their various states. + * It uses path relative to the library, so it can moved without issue + * + * It also provides the endpoints with information about unmatched games + */ + +import fs from "fs"; +import path from "path"; +import prisma from "../db/database"; +import { GameVersion, Platform } from "@prisma/client"; +import { fuzzy } from "fast-fuzzy"; +import { recursivelyReaddir } from "../utils/recursivedirs"; + +class LibraryManager { + private basePath: string; + + constructor() { + this.basePath = process.env.LIBRARY ?? "./.data/library"; + } + + async fetchAllUnimportedGames() { + const dirs = fs.readdirSync(this.basePath).filter((e) => { + const fullDir = path.join(this.basePath, e); + return fs.lstatSync(fullDir).isDirectory(); + }); + + const validGames = await prisma.game.findMany({ + where: { + libraryBasePath: { in: dirs }, + }, + select: { + libraryBasePath: true, + }, + }); + const validGameDirs = validGames.map((e) => e.libraryBasePath); + + const unregisteredGames = dirs.filter((e) => !validGameDirs.includes(e)); + + return unregisteredGames; + } + + async fetchUnimportedGameVersions( + libraryBasePath: string, + versions: Array + ) { + const gameDir = path.join(this.basePath, libraryBasePath); + const versionsDirs = fs.readdirSync(gameDir); + const importedVersionDirs = versions.map((e) => e.versionName); + const unimportedVersions = versionsDirs.filter( + (e) => !importedVersionDirs.includes(e) + ); + + return unimportedVersions; + } + + async fetchGamesWithStatus() { + const games = await prisma.game.findMany({ + select: { + id: true, + versions: true, + mName: true, + mShortDescription: true, + metadataSource: true, + mDevelopers: true, + mPublishers: true, + mIconId: true, + libraryBasePath: true, + }, + }); + + return await Promise.all( + games.map(async (e) => ({ + game: e, + status: { + noVersions: e.versions.length == 0, + unimportedVersions: await this.fetchUnimportedGameVersions( + e.libraryBasePath, + e.versions + ), + }, + })) + ); + } + + async fetchUnimportedVersions(gameId: string) { + const game = await prisma.game.findUnique({ + where: { id: gameId }, + select: { + versions: { + select: { + versionName: true, + }, + }, + libraryBasePath: true, + }, + }); + + if (!game) return undefined; + const targetDir = path.join(this.basePath, game.libraryBasePath); + if (!fs.existsSync(targetDir)) + throw new Error( + "Game in database, but no physical directory? Something is very very wrong..." + ); + const versions = fs.readdirSync(targetDir); + const currentVersions = game.versions.map((e) => e.versionName); + + const unimportedVersions = versions.filter( + (e) => !currentVersions.includes(e) + ); + return unimportedVersions; + } + + async fetchUnimportedVersionInformation(gameId: string, versionName: string) { + const game = await prisma.game.findUnique({ + where: { id: gameId }, + select: { libraryBasePath: true, mName: true }, + }); + if (!game) return undefined; + const targetDir = path.join( + this.basePath, + game.libraryBasePath, + versionName + ); + if (!fs.existsSync(targetDir)) return undefined; + + const fileExts: { [key: string]: string[] } = { + Linux: [ + // Ext for Unity games + ".x86_64", + // No extension is common for Linux binaries + "", + ], + Windows: [ + // Pretty much the only one + ".exe", + ], + }; + + const options: Array<{ + filename: string; + platform: string; + match: number; + }> = []; + + const files = recursivelyReaddir(targetDir); + for (const file of files) { + const filename = path.basename(file); + const dotLocation = file.lastIndexOf("."); + const ext = dotLocation == -1 ? "" : file.slice(dotLocation); + for (const [platform, checkExts] of Object.entries(fileExts)) { + for (const checkExt of checkExts) { + if (checkExt != ext) continue; + const fuzzyValue = fuzzy(filename, game.mName); + options.push({ + filename: file, + platform: platform, + match: fuzzyValue, + }); + } + } + } + + const sortedOptions = options.sort((a, b) => b.match - a.match); + let startupGuess = ""; + let platformGuess = ""; + if (sortedOptions.length > 0) { + const finalChoice = sortedOptions[0]; + const finalChoiceRelativePath = path.relative( + targetDir, + finalChoice.filename + ); + startupGuess = finalChoiceRelativePath; + platformGuess = finalChoice.platform; + } + + return { startupGuess, platformGuess }; + } + + // Checks are done in least to most expensive order + async checkUnimportedGamePath(targetPath: string) { + const targetDir = path.join(this.basePath, targetPath); + if (!fs.existsSync(targetDir)) return false; + + const hasGame = + (await prisma.game.count({ where: { libraryBasePath: targetPath } })) > 0; + if (hasGame) return false; + + return true; + } +} + +export const libraryManager = new LibraryManager(); +export default libraryManager; diff --git a/server/internal/metadata/giantbomb.ts b/server/internal/metadata/giantbomb.ts index 289ae9f..1f2b426 100644 --- a/server/internal/metadata/giantbomb.ts +++ b/server/internal/metadata/giantbomb.ts @@ -50,9 +50,10 @@ interface GameResult { interface CompanySearchResult { guid: string, - deck: string, - description: string, + deck: string | null, + description: string | null, name: string, + website: string | null, image: { icon_url: string, @@ -191,8 +192,9 @@ export class GiantBombProvider implements MetadataProvider { const metadata: PublisherMetadata = { id: company.guid, name: company.name, - shortDescription: company.deck, - description: longDescription, + shortDescription: company.deck ?? "", + description: longDescription ?? "", + website: company.website ?? "", logo: createObject(company.image.icon_url), banner: createObject(company.image.screen_large_url), diff --git a/server/internal/metadata/index.ts b/server/internal/metadata/index.ts index 7f6f0e3..834b99d 100644 --- a/server/internal/metadata/index.ts +++ b/server/internal/metadata/index.ts @@ -1,162 +1,215 @@ -import { Developer, MetadataSource, PrismaClient, Publisher } from "@prisma/client"; +import { + Developer, + MetadataSource, + PrismaClient, + Publisher, +} from "@prisma/client"; import prisma from "../db/database"; -import { _FetchDeveloperMetadataParams, _FetchGameMetadataParams, _FetchPublisherMetadataParams, DeveloperMetadata, GameMetadata, GameMetadataSearchResult, InternalGameMetadataResult, PublisherMetadata } from "./types"; +import { + _FetchDeveloperMetadataParams, + _FetchGameMetadataParams, + _FetchPublisherMetadataParams, + DeveloperMetadata, + GameMetadata, + GameMetadataSearchResult, + InternalGameMetadataResult, + PublisherMetadata, +} from "./types"; import { ObjectTransactionalHandler } from "../objects/transactional"; import { PriorityList, PriorityListIndexed } from "../utils/prioritylist"; export abstract class MetadataProvider { - abstract id(): string; - abstract name(): string; - abstract source(): MetadataSource; + abstract id(): string; + abstract name(): string; + abstract source(): MetadataSource; - abstract search(query: string): Promise; - abstract fetchGame(params: _FetchGameMetadataParams): Promise; - abstract fetchPublisher(params: _FetchPublisherMetadataParams): Promise; - abstract fetchDeveloper(params: _FetchDeveloperMetadataParams): Promise; + abstract search(query: string): Promise; + abstract fetchGame(params: _FetchGameMetadataParams): Promise; + abstract fetchPublisher( + params: _FetchPublisherMetadataParams + ): Promise; + abstract fetchDeveloper( + params: _FetchDeveloperMetadataParams + ): Promise; } export class MetadataHandler { - // Ordered by priority - private providers: PriorityListIndexed = new PriorityListIndexed("id"); - private objectHandler: ObjectTransactionalHandler = new ObjectTransactionalHandler(); + // Ordered by priority + private providers: PriorityListIndexed = + new PriorityListIndexed("id"); + private objectHandler: ObjectTransactionalHandler = + new ObjectTransactionalHandler(); - addProvider(provider: MetadataProvider, priority: number = 0) { - this.providers.push(provider, priority); - } + addProvider(provider: MetadataProvider, priority: number = 0) { + this.providers.push(provider, priority); + } - async search(query: string) { - const promises: Promise[] = []; - for (const provider of this.providers.values()) { - const queryTransformationPromise = new Promise(async (resolve, reject) => { - const results = await provider.search(query); - const mappedResults: InternalGameMetadataResult[] = results.map((result) => Object.assign( - {}, - result, - { - sourceId: provider.id(), - sourceName: provider.name() - } - )); - resolve(mappedResults); - }); - promises.push(queryTransformationPromise); - } - - const results = await Promise.allSettled(promises); - const successfulResults = results.filter((result) => result.status === 'fulfilled').map((result) => result.value).flat(); - - return successfulResults; - } - - async fetchGame(result: InternalGameMetadataResult) { - const provider = this.providers.get(result.sourceId); - if (!provider) throw new Error(`Invalid metadata provider for ID "${result.sourceId}"`); - - const existing = await prisma.game.findUnique({ - where: { - metadataKey: { - metadataSource: provider.source(), - metadataId: provider.id(), - } - } - }); - if (existing) return existing; - - const [createObject, pullObjects, dumpObjects] = this.objectHandler.new(); - - let metadata; - try { - metadata = await provider.fetchGame({ - id: result.id, - publisher: this.fetchPublisher, - developer: this.fetchDeveloper, - createObject, + async search(query: string) { + const promises: Promise[] = []; + for (const provider of this.providers.values()) { + const queryTransformationPromise = new Promise< + InternalGameMetadataResult[] + >(async (resolve, reject) => { + const results = await provider.search(query); + const mappedResults: InternalGameMetadataResult[] = results.map( + (result) => + Object.assign({}, result, { + sourceId: provider.id(), + sourceName: provider.name(), }) - } catch (e) { - dumpObjects(); - throw e; - } - - await pullObjects(); - const game = await prisma.game.create({ - data: { - metadataSource: provider.source(), - metadataId: metadata.id, - - mName: metadata.name, - mShortDescription: metadata.shortDescription, - mDescription: metadata.description, - mDevelopers: { - connect: metadata.developers - }, - mPublishers: { - connect: metadata.publishers, - }, - - mReviewCount: metadata.reviewCount, - mReviewRating: metadata.reviewRating, - - mIconId: metadata.icon, - mBannerId: metadata.banner, - mArt: metadata.art, - mScreenshots: metadata.screenshots, - }, - }); - - return game; + ); + resolve(mappedResults); + }); + promises.push(queryTransformationPromise); } - async fetchDeveloper(query: string) { - return await this.fetchDeveloperPublisher(query, "fetchDeveloper", "developer") as Developer; + const results = await Promise.allSettled(promises); + const successfulResults = results + .filter((result) => result.status === "fulfilled") + .map((result) => result.value) + .flat(); + + return successfulResults; + } + + async createGame( + result: InternalGameMetadataResult, + libraryBasePath: string + ) { + const provider = this.providers.get(result.sourceId); + if (!provider) + throw new Error(`Invalid metadata provider for ID "${result.sourceId}"`); + + const existing = await prisma.game.findUnique({ + where: { + metadataKey: { + metadataSource: provider.source(), + metadataId: provider.id(), + }, + }, + }); + if (existing) return existing; + + const [createObject, pullObjects, dumpObjects] = this.objectHandler.new( + {}, + ["internal:read"] + ); + + let metadata; + try { + metadata = await provider.fetchGame({ + id: result.id, + // wrap in anonymous functions to keep references to this + publisher: (name: string) => this.fetchPublisher(name), + developer: (name: string) => this.fetchDeveloper(name), + createObject, + }); + } catch (e) { + dumpObjects(); + throw e; } - async fetchPublisher(query: string) { - return await this.fetchDeveloperPublisher(query, "fetchPublisher", "publisher") as Publisher; + await pullObjects(); + const game = await prisma.game.create({ + data: { + metadataSource: provider.source(), + metadataId: metadata.id, + + mName: metadata.name, + mShortDescription: metadata.shortDescription, + mDescription: metadata.description, + mDevelopers: { + connect: metadata.developers, + }, + mPublishers: { + connect: metadata.publishers, + }, + + mReviewCount: metadata.reviewCount, + mReviewRating: metadata.reviewRating, + + mIconId: metadata.icon, + mBannerId: metadata.banner, + mArt: metadata.art, + mScreenshots: metadata.screenshots, + + versionOrder: [], + libraryBasePath, + }, + }); + + return game; + } + + async fetchDeveloper(query: string) { + return (await this.fetchDeveloperPublisher( + query, + "fetchDeveloper", + "developer" + )) as Developer; + } + + async fetchPublisher(query: string) { + return (await this.fetchDeveloperPublisher( + query, + "fetchPublisher", + "publisher" + )) as Publisher; + } + + // Careful with this function, it has no typechecking + // TODO: fix typechecking + private async fetchDeveloperPublisher( + query: string, + functionName: any, + databaseName: any + ) { + const existing = await (prisma as any)[databaseName].findFirst({ + where: { + metadataOriginalQuery: query, + }, + }); + if (existing) return existing; + + for (const provider of this.providers.values() as any) { + const [createObject, pullObjects, dumpObjects] = this.objectHandler.new( + {}, + ["internal:read"] + ); + let result; + try { + result = await provider[functionName]({ query, createObject }); + } catch(e) { + console.warn(e); + dumpObjects(); + continue; + } + + // If we're successful + await pullObjects(); + + const object = await (prisma as any)[databaseName].create({ + data: { + metadataSource: provider.source(), + metadataId: provider.id(), + metadataOriginalQuery: query, + + mName: result.name, + mShortDescription: result.shortDescription, + mDescription: result.description, + mLogo: result.logo, + mBanner: result.banner, + mWebsite: result.website, + }, + }); + + return object; } - // Careful with this function, it has no typechecking - // TODO: fix typechecking - private async fetchDeveloperPublisher(query: string, functionName: any, databaseName: any) { - const existing = await (prisma as any)[databaseName].findFirst({ - where: { - mName: query, - } - }); - if (existing) return existing; - - for (const provider of this.providers.values() as any) { - const [createObject, pullObjects, dumpObjects] = this.objectHandler.new(); - let result; - try { - result = await provider[functionName]({ query, createObject }); - } catch { - dumpObjects(); - continue; - } - - // If we're successful - await pullObjects(); - - const object = await (prisma as any)[databaseName].create({ - data: { - metadataSource: provider.source(), - metadataId: provider.id(), - - mName: result.name, - mShortDescription: result.shortDescription, - mDescription: result.description, - mLogo: result.logo, - mBanner: result.banner, - }, - }) - - return object; - - } - - throw new Error(`No metadata provider found a ${databaseName} for "${query}"`); - - } + throw new Error( + `No metadata provider found a ${databaseName} for "${query}"` + ); + } } -export default new MetadataHandler(); \ No newline at end of file +export default new MetadataHandler(); diff --git a/server/internal/metadata/types.d.ts b/server/internal/metadata/types.d.ts index 8e3d00d..cb175bf 100644 --- a/server/internal/metadata/types.d.ts +++ b/server/internal/metadata/types.d.ts @@ -45,6 +45,7 @@ export interface PublisherMetadata { logo: ObjectReference; banner: ObjectReference; + website: String; } export type DeveloperMetadata = PublisherMetadata; diff --git a/server/internal/objects/fsBackend.ts b/server/internal/objects/fsBackend.ts index 23f8ccc..9ce0d31 100644 --- a/server/internal/objects/fsBackend.ts +++ b/server/internal/objects/fsBackend.ts @@ -45,10 +45,10 @@ export class FsObjectBackend extends ObjectBackend { return false; } async create( + id: string, source: Source, metadata: ObjectMetadata ): Promise { - const id = uuidv4(); const objectPath = path.join(this.baseObjectPath, sanitize(id)); const metadataPath = path.join( this.baseMetadataPath, diff --git a/server/internal/objects/index.ts b/server/internal/objects/index.ts index f2d1028..1a6b450 100644 --- a/server/internal/objects/index.ts +++ b/server/internal/objects/index.ts @@ -17,6 +17,7 @@ import { parse as getMimeTypeBuffer } from "file-type-mime"; import { Readable } from "stream"; import { getMimeType as getMimeTypeStream } from "stream-mime-type"; +import { v4 as uuidv4 } from "uuid"; export type ObjectReference = string; export type ObjectMetadata = { @@ -46,6 +47,7 @@ export abstract class ObjectBackend { abstract fetch(id: ObjectReference): Promise; abstract write(id: ObjectReference, source: Source): Promise; abstract create( + id: string, source: Source, metadata: ObjectMetadata ): Promise; @@ -59,6 +61,7 @@ export abstract class ObjectBackend { ): Promise; async createFromSource( + id: string, sourceFetcher: () => Promise, metadata: { [key: string]: string }, permissions: Array @@ -83,13 +86,11 @@ export abstract class ObjectBackend { if (!mime) throw new Error("Unable to calculate MIME type - is the source empty?"); - const objectId = this.create(source, { + await this.create(id, source, { permissions, userMetadata: metadata, mime, }); - - return objectId; } async fetchWithPermissions(id: ObjectReference, userId?: string) { diff --git a/server/internal/objects/transactional.ts b/server/internal/objects/transactional.ts index 767b81d..aabb7cd 100644 --- a/server/internal/objects/transactional.ts +++ b/server/internal/objects/transactional.ts @@ -2,7 +2,9 @@ The purpose of this class is to hold references to remote objects (like images) until they're actually needed This is used as a utility in metadata handling, so we only fetch the objects if we're actually creating a database record. */ -import { v4 as uuidv4 } from 'uuid'; +import { Readable } from "stream"; +import { v4 as uuidv4 } from "uuid"; +import { GlobalObjectHandler } from "~/server/plugins/objects"; type TransactionTable = { [key: string]: string }; // ID to URL type GlobalTransactionRecord = { [key: string]: TransactionTable }; // Transaction ID to table @@ -12,27 +14,38 @@ type Pull = () => Promise; type Dump = () => void; export class ObjectTransactionalHandler { - private record: GlobalTransactionRecord = {}; + private record: GlobalTransactionRecord = {}; - new(): [Register, Pull, Dump] { - const transactionId = uuidv4(); + new( + metadata: { [key: string]: string }, + permissions: Array + ): [Register, Pull, Dump] { + const transactionId = uuidv4(); - const register = (url: string) => { - const objectId = uuidv4(); - this.record[transactionId][objectId] = url; + this.record[transactionId] ??= {}; - return objectId; - } + const register = (url: string) => { + const objectId = uuidv4(); + this.record[transactionId][objectId] = url; - const pull = async () => { - // Dummy function - dump(); - } + return objectId; + }; - const dump = () => { - delete this.record[transactionId]; - } + const pull = async () => { + for (const [id, url] of Object.entries(this.record[transactionId])) { + await GlobalObjectHandler.createFromSource( + id, + () => $fetch(url, { responseType: "stream" }), + metadata, + permissions + ); + } + }; - return [register, pull, dump]; - } -} \ No newline at end of file + const dump = () => { + delete this.record[transactionId]; + }; + + return [register, pull, dump]; + } +} diff --git a/server/internal/session/index.ts b/server/internal/session/index.ts index 794dadd..713727b 100644 --- a/server/internal/session/index.ts +++ b/server/internal/session/index.ts @@ -13,52 +13,71 @@ const userSessionKey = "_userSession"; const userIdKey = "_userId"; export class SessionHandler { - private sessionProvider: SessionProvider; + private sessionProvider: SessionProvider; - constructor() { - // Create a new provider - this.sessionProvider = createMemorySessionProvider(); + constructor() { + // Create a new provider + this.sessionProvider = createMemorySessionProvider(); + } + + async getSession(h3: H3Event) { + const data = await this.sessionProvider.getSession<{ [userSessionKey]: T }>( + h3 + ); + if (!data) return undefined; + + return data[userSessionKey]; + } + async setSession(h3: H3Event, data: any, expend = false) { + const result = await this.sessionProvider.updateSession( + h3, + userSessionKey, + data + ); + if (!result) { + const toCreate = { [userSessionKey]: data }; + await this.sessionProvider.setSession(h3, toCreate, expend); } + } + async clearSession(h3: H3Event) { + await this.sessionProvider.clearSession(h3); + } - async getSession(h3: H3Event) { - const data = await this.sessionProvider.getSession<{ [userSessionKey]: T }>(h3); - if (!data) return undefined; + async getUserId(h3: H3Event) { + const session = await this.sessionProvider.getSession<{ + [userIdKey]: string | undefined; + }>(h3); + if (!session) return undefined; - return data[userSessionKey]; - } - async setSession(h3: H3Event, data: any, expend = false) { - const result = await this.sessionProvider.updateSession(h3, userSessionKey, data); - if (!result) { - const toCreate = { [userSessionKey]: data }; - await this.sessionProvider.setSession(h3, toCreate, expend); - } - } - async clearSession(h3: H3Event) { - await this.sessionProvider.clearSession(h3); + return session[userIdKey]; + } + + async getUser(h3: H3Event) { + const userId = await this.getUserId(h3); + if (!userId) return undefined; + + const user = await prisma.user.findFirst({ where: { id: userId } }); + return user; + } + + async setUserId(h3: H3Event, userId: string, extend = false) { + const result = await this.sessionProvider.updateSession( + h3, + userIdKey, + userId + ); + if (!result) { + const toCreate = { [userIdKey]: userId }; + await this.sessionProvider.setSession(h3, toCreate, extend); } + } - async getUserId(h3: H3Event) { - const session = await this.sessionProvider.getSession<{ [userIdKey]: string | undefined }>(h3); - if (!session) return undefined; - - return session[userIdKey]; - } - - async getUser(h3: H3Event) { - const userId = await this.getUserId(h3); - if (!userId) return undefined; - - const user = await prisma.user.findFirst({ where: { id: userId } }); - return user; - } - - async setUserId(h3: H3Event, userId: string, extend = false) { - const result = await this.sessionProvider.updateSession(h3, userIdKey, userId); - if (!result) { - const toCreate = { [userIdKey]: userId }; - await this.sessionProvider.setSession(h3, toCreate, extend); - } - } + async getAdminUser(h3: H3Event) { + const user = await this.getUser(h3); + if (!user) return undefined; + if (!user.admin) return undefined; + return user; + } } -export default new SessionHandler(); \ No newline at end of file +export default new SessionHandler(); diff --git a/server/internal/utils/parseplatform.ts b/server/internal/utils/parseplatform.ts new file mode 100644 index 0000000..b94fed5 --- /dev/null +++ b/server/internal/utils/parseplatform.ts @@ -0,0 +1,14 @@ +import { Platform } from "@prisma/client"; + +export function parsePlatform(platform: string) { + switch (platform) { + case "linux": + case "Linux": + return Platform.Linux; + case "windows": + case "Windows": + return Platform.Windows; + } + + return undefined; +} diff --git a/server/internal/utils/recursivedirs.ts b/server/internal/utils/recursivedirs.ts new file mode 100644 index 0000000..a5a10a6 --- /dev/null +++ b/server/internal/utils/recursivedirs.ts @@ -0,0 +1,20 @@ +import fs from "fs"; +import path from "path"; + +export function recursivelyReaddir(dir: string) { + const result: Array = []; + const files = fs.readdirSync(dir); + for (const file of files) { + const targetDir = path.join(dir, file); + const stat = fs.lstatSync(targetDir); + if (stat.isDirectory()) { + const subdirs = recursivelyReaddir(targetDir); + const subdirsWithBase = subdirs.map((e) => path.join(dir, e)); + result.push(...subdirsWithBase); + continue; + } + result.push(targetDir); + } + + return result; +} diff --git a/server/plugins/objects.ts b/server/plugins/objects.ts index 42d592f..8d41cc0 100644 --- a/server/plugins/objects.ts +++ b/server/plugins/objects.ts @@ -1,11 +1,10 @@ import { FsObjectBackend } from "../internal/objects/fsBackend"; +// To-do insert logic surrounding deciding what object backend to use +export const GlobalObjectHandler = new FsObjectBackend(); + export default defineNitroPlugin((nitro) => { - const currentObjectHandler = new FsObjectBackend(); - - // To-do insert logic surrounding deciding what object backend to use - nitro.hooks.hook("request", (h3) => { - h3.context.objects = currentObjectHandler; + h3.context.objects = GlobalObjectHandler; }); }); diff --git a/yarn.lock b/yarn.lock index 6294ef6..af5f09e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -296,23 +296,23 @@ dependencies: mime "^3.0.0" -"@drop/droplet-linux-x64-gnu@0.3.2": - version "0.3.2" - resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-linux-x64-gnu/-/@drop/droplet-linux-x64-gnu-0.3.2.tgz#9be12f1e61df67837bb225ec67cc98f5af8f703b" - integrity sha1-m+EvHmHfZ4N7siXsZ8yY9a+PcDs= +"@drop/droplet-linux-x64-gnu@0.4.1", "@drop/droplet-linux-x64-gnu@^0.4.1": + version "0.4.1" + resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-linux-x64-gnu/-/@drop/droplet-linux-x64-gnu-0.4.1.tgz#24f9ccebf7349bec450b855571b300284fb3731f" + integrity sha1-JPnM6/c0m+xFC4VVcbMAKE+zcx8= -"@drop/droplet-win32-x64-msvc@0.3.2": - version "0.3.2" - resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-win32-x64-msvc/-/@drop/droplet-win32-x64-msvc-0.3.2.tgz#dc5d0fa8334bf211666e99ca365c900d363f7823" - integrity sha1-3F0PqDNL8hFmbpnKNlyQDTY/eCM= +"@drop/droplet-win32-x64-msvc@0.4.1", "@drop/droplet-win32-x64-msvc@^0.4.1": + version "0.4.1" + resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-win32-x64-msvc/-/@drop/droplet-win32-x64-msvc-0.4.1.tgz#58238faca15b36abb02162354c2f39526bc213a1" + integrity sha1-WCOPrKFbNquwIWI1TC85UmvCE6E= -"@drop/droplet@^0.3.2": - version "0.3.2" - resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet/-/@drop/droplet-0.3.2.tgz#f57cf35e50dfd448b7837f9b4712543ff160e769" - integrity sha1-9XzzXlDf1Ei3g3+bRxJUP/Fg52k= +"@drop/droplet@^0.4.1": + version "0.4.1" + resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet/-/@drop/droplet-0.4.1.tgz#d4f3a7950fad2a95487ce4c014e1c782c2fcc3c7" + integrity sha1-1POnlQ+tKpVIfOTAFOHHgsL8w8c= optionalDependencies: - "@drop/droplet-linux-x64-gnu" "0.3.2" - "@drop/droplet-win32-x64-msvc" "0.3.2" + "@drop/droplet-linux-x64-gnu" "0.4.1" + "@drop/droplet-win32-x64-msvc" "0.4.1" "@esbuild/aix-ppc64@0.20.2": version "0.20.2" @@ -2709,6 +2709,13 @@ fast-fifo@^1.2.0, fast-fifo@^1.3.2: resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== +fast-fuzzy@^1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/fast-fuzzy/-/fast-fuzzy-1.12.0.tgz#f900a8165bffbb7dd5c013a1bb96ee22179ba406" + integrity sha512-sXxGgHS+ubYpsdLnvOvJ9w5GYYZrtL9mkosG3nfuD446ahvoWEsSKBP7ieGmWIKVLnaxRDgUJkZMdxRgA2Ni+Q== + dependencies: + graphemesplit "^2.4.1" + fast-glob@^3.2.7, fast-glob@^3.3.0, fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" @@ -2983,6 +2990,14 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +graphemesplit@^2.4.1: + version "2.4.4" + resolved "https://registry.yarnpkg.com/graphemesplit/-/graphemesplit-2.4.4.tgz#6d325c61e928efdaec2189f54a9b87babf89b75a" + integrity sha512-lKrpp1mk1NH26USxC/Asw4OHbhSQf5XfrWZ+CDv/dFVvd1j17kFgMotdJvOesmHkbFX9P9sBfpH8VogxOWLg8w== + dependencies: + js-base64 "^3.6.0" + unicode-trie "^2.0.0" + gzip-size@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-7.0.0.tgz#9f9644251f15bc78460fccef4055ae5a5562ac60" @@ -3306,6 +3321,11 @@ jiti@^2.0.0: resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.0.0.tgz#ccaab6ce73a73cbf04e187645c614b3a3d41b653" integrity sha512-CJ7e7Abb779OTRv3lomfp7Mns/Sy1+U4pcAx5VbjxCZD5ZM/VJaXPpPjNKjtSvWQy/H86E49REXR34dl1JEz9w== +js-base64@^3.6.0: + version "3.7.7" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79" + integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -3838,7 +3858,7 @@ nuxi@^3.13.2: resolved "https://registry.yarnpkg.com/nuxi/-/nuxi-3.14.0.tgz#697a1e8b4f0d92fb8b30aa355af9295fa8c2cb4e" integrity sha512-MhG4QR6D95jQxhnwKfdKXulZ8Yqy1nbpwbotbxY5IcabOzpEeTB8hYn2BFkmYdMUB0no81qpv2ldZmVCT9UsnQ== -nuxt@^3.13.0: +nuxt@^3.13.2: version "3.13.2" resolved "https://registry.yarnpkg.com/nuxt/-/nuxt-3.13.2.tgz#af43a1fb5ccaaf98be0aaeca1bee504eeee24135" integrity sha512-Bjc2qRsipfBhjXsBEJCN+EUAukhdgFv/KoIR5HFB2hZOYRSqXBod3oWQs78k3ja1nlIhAEdBG533898KJxUtJw== @@ -4006,6 +4026,11 @@ package-manager-detector@^0.2.0: resolved "https://registry.yarnpkg.com/package-manager-detector/-/package-manager-detector-0.2.0.tgz#160395cd5809181f5a047222319262b8c2d8aaea" integrity sha512-E385OSk9qDcXhcM9LNSe4sdhx8a9mAPrZ4sMLW+tmxl5ZuGtPUcdFu+MPP2jbgiWAZ6Pfe5soGFMd+0Db5Vrog== +pako@^0.2.5: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA== + parse-git-config@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/parse-git-config/-/parse-git-config-3.0.0.tgz#4a2de08c7b74a2555efa5ae94d40cd44302a6132" @@ -5085,6 +5110,11 @@ through2@4.0.2: dependencies: readable-stream "3" +tiny-inflate@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" + integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== + tiny-invariant@^1.1.0: version "1.3.3" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" @@ -5220,6 +5250,14 @@ unhead@1.11.6, unhead@^1.11.5: "@unhead/shared" "1.11.6" hookable "^5.5.3" +unicode-trie@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-trie/-/unicode-trie-2.0.0.tgz#8fd8845696e2e14a8b67d78fa9e0dd2cad62fec8" + integrity sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ== + dependencies: + pako "^0.2.5" + tiny-inflate "^1.0.0" + unicorn-magic@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz#1bb9a51c823aaf9d73a8bfcd3d1a23dde94b0ce4"