Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions components/GameEditor/Version.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,17 @@
<!-- import games button -->

<NuxtLink
:href="
unimportedVersions.length > 0
? `/admin/library/${game.id}/import`
: ''
"
:href="canImport ? `/admin/library/${game.id}/import` : ''"
type="button"
:class="[
unimportedVersions.length > 0
canImport
? 'bg-blue-600 hover:bg-blue-700'
: 'bg-blue-800/50',
'inline-flex w-fit items-center gap-x-2 rounded-md px-3 py-1 text-sm font-semibold font-display text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600',
]"
>
{{
unimportedVersions.length > 0
canImport
? $t("library.admin.import.version.import")
: $t("library.admin.import.version.noVersions")
}}
Expand Down Expand Up @@ -124,10 +120,16 @@ import { ExclamationCircleIcon } from "@heroicons/vue/24/outline";

// TODO implement UI for this page

defineProps<{ unimportedVersions: string[] }>();
const props = defineProps<{ unimportedVersions: string[] }>();

const { t } = useI18n();

const hasDeleted = ref(false);

const canImport = computed(
() => hasDeleted.value || props.unimportedVersions.length > 0,
);

type GameAndVersions = GameModel & { versions: GameVersionModel[] };
const game = defineModel<SerializeObject<GameAndVersions>>() as Ref<
SerializeObject<GameAndVersions>
Expand Down Expand Up @@ -176,6 +178,7 @@ async function deleteVersion(versionName: string) {
game.value.versions.findIndex((e) => e.versionName === versionName),
1,
);
hasDeleted.value = true;
} catch (e) {
createModal(
ModalType.Notification,
Expand Down
8 changes: 6 additions & 2 deletions components/LanguageSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@
</i18n-t>
</NuxtLink>

<DevOnly
><h1 class="mt-3 text-sm text-gray-500">{{ $t("welcome") }}</h1>
<DevOnly>
<h1 class="mt-3 text-sm text-gray-500">{{ $t("welcome") }}</h1>
</DevOnly>
</div>
</template>

<script setup lang="ts">
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/24/outline";
</script>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
},
"dependencies": {
"@discordapp/twemoji": "^16.0.1",
"@drop-oss/droplet": "1.6.0",
"@drop-oss/droplet": "2.3.0",
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5",
"@lobomfz/prismark": "0.0.3",
Expand Down
4 changes: 2 additions & 2 deletions pages/admin/task/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
/>
</div>
<p class="mt-1 truncate text-sm text-zinc-400">
{{ parseTaskLog(task.value.log.at(-1) ?? "").message }}
{{ parseTaskLog(task.value.log.at(-1)).message }}
</p>
<NuxtLink
type="button"
Expand Down Expand Up @@ -115,7 +115,7 @@
{{ task.id }}
</p>
<p class="mt-1 truncate text-sm text-zinc-400">
{{ parseTaskLog(task.log.at(-1) ?? "").message }}
{{ parseTaskLog(task.log.at(-1)).message }}
</p>
<NuxtLink
type="button"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
Warnings:

- The primary key for the `Task` table will be changed. If it partially fails, the table could be left without primary key constraint.

*/
-- DropIndex
DROP INDEX "GameTag_name_idx";

-- AlterTable
ALTER TABLE "Task" DROP CONSTRAINT "Task_pkey",
ADD CONSTRAINT "Task_pkey" PRIMARY KEY ("id", "started");

-- CreateIndex
CREATE INDEX "GameTag_name_idx" ON "GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));
4 changes: 3 additions & 1 deletion prisma/models/task.prisma
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
model Task {
id String @id
id String
taskGroup String
name String

Expand All @@ -12,4 +12,6 @@ model Task {
log String[]

acls String[]

@@id([id, started])
}
13 changes: 13 additions & 0 deletions server/api/v2/client/chunk.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type } from "arktype";
import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
import contextManager from "~/server/internal/downloads/coordinator";
import libraryManager from "~/server/internal/library";
import { logger } from "~/server/internal/logging";

const GetChunk = type({
context: "string",
Expand Down Expand Up @@ -58,13 +59,25 @@ export default defineEventHandler(async (h3) => {
statusCode: 500,
statusMessage: "Failed to create read stream",
});
let length = 0;
await gameReadStream.pipeTo(
new WritableStream({
write(chunk) {
h3.node.res.write(chunk);
length += chunk.length;
},
}),
);

if (length != file.end - file.start) {
logger.warn(
`failed to read enough from ${file.filename}. read ${length}, required: ${file.end - file.start}`,
);
throw createError({
statusCode: 500,
statusMessage: "Failed to read enough from stream.",
});
}
}

await h3.node.res.end();
Expand Down
97 changes: 16 additions & 81 deletions server/internal/library/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ import { GameNotFoundError, type LibraryProvider } from "./provider";
import { logger } from "../logging";
import type { GameModel } from "~/prisma/client/models";

export function createGameImportTaskId(libraryId: string, libraryPath: string) {
return btoa(`import:${libraryId}:${libraryPath}`);
}

export function createVersionImportTaskId(gameId: string, versionName: string) {
return btoa(`import:${gameId}:${versionName}`);
}

class LibraryManager {
private libraries: Map<string, LibraryProvider<unknown>> = new Map();

private gameImportLocks: Map<string, Array<string>> = new Map(); // Library ID to Library Path
private versionImportLocks: Map<string, Array<string>> = new Map(); // Game ID to Version Name

addLibrary(library: LibraryProvider<unknown>) {
this.libraries.set(library.id(), library);
}
Expand Down Expand Up @@ -58,12 +63,10 @@ class LibraryManager {

for (const [id, library] of this.libraries.entries()) {
const providerGames = await library.listGames();
const locks = this.gameImportLocks.get(id) ?? [];
const providerUnimportedGames = providerGames.filter(
(libraryPath) =>
instanceGames[id] &&
!instanceGames[id][libraryPath] &&
!locks.includes(libraryPath),
!instanceGames[id]?.[libraryPath] &&
!taskHandler.hasTask(createGameImportTaskId(id, libraryPath)),
);
unimportedGames[id] = providerUnimportedGames;
}
Expand Down Expand Up @@ -93,7 +96,7 @@ class LibraryManager {
const unimportedVersions = versions.filter(
(e) =>
game.versions.findIndex((v) => v.versionName == e) == -1 &&
!(this.versionImportLocks.get(game.id) ?? []).includes(e),
!taskHandler.hasTask(createVersionImportTaskId(game.id, e)),
);
return unimportedVersions;
} catch (e) {
Expand Down Expand Up @@ -177,7 +180,8 @@ class LibraryManager {
for (const filename of files) {
const basename = path.basename(filename);
const dotLocation = filename.lastIndexOf(".");
const ext = dotLocation == -1 ? "" : filename.slice(dotLocation);
const ext =
dotLocation == -1 ? "" : filename.slice(dotLocation).toLowerCase();
for (const [platform, checkExts] of Object.entries(fileExts)) {
for (const checkExt of checkExts) {
if (checkExt != ext) continue;
Expand Down Expand Up @@ -215,70 +219,6 @@ class LibraryManager {
}
*/

/**
* Locks the game so you can't be imported
* @param libraryId
* @param libraryPath
*/
async lockGame(libraryId: string, libraryPath: string) {
let games = this.gameImportLocks.get(libraryId);
if (!games) this.gameImportLocks.set(libraryId, (games = []));

if (!games.includes(libraryPath)) games.push(libraryPath);

this.gameImportLocks.set(libraryId, games);
}

/**
* Unlocks the game, call once imported
* @param libraryId
* @param libraryPath
*/
async unlockGame(libraryId: string, libraryPath: string) {
let games = this.gameImportLocks.get(libraryId);
if (!games) this.gameImportLocks.set(libraryId, (games = []));

if (games.includes(libraryPath))
games.splice(
games.findIndex((e) => e === libraryPath),
1,
);

this.gameImportLocks.set(libraryId, games);
}

/**
* Locks a version so it can't be imported
* @param gameId
* @param versionName
*/
async lockVersion(gameId: string, versionName: string) {
let versions = this.versionImportLocks.get(gameId);
if (!versions) this.versionImportLocks.set(gameId, (versions = []));

if (!versions.includes(versionName)) versions.push(versionName);

this.versionImportLocks.set(gameId, versions);
}

/**
* Unlocks the version, call once imported
* @param libraryId
* @param libraryPath
*/
async unlockVersion(gameId: string, versionName: string) {
let versions = this.versionImportLocks.get(gameId);
if (!versions) this.versionImportLocks.set(gameId, (versions = []));

if (versions.includes(gameId))
versions.splice(
versions.findIndex((e) => e === versionName),
1,
);

this.versionImportLocks.set(gameId, versions);
}

async importVersion(
gameId: string,
versionName: string,
Expand All @@ -295,7 +235,7 @@ class LibraryManager {
umuId: string;
},
) {
const taskId = `import:${gameId}:${versionName}`;
const taskId = createVersionImportTaskId(gameId, versionName);

const platform = parsePlatform(metadata.platform);
if (!platform) return undefined;
Expand All @@ -309,8 +249,6 @@ class LibraryManager {
const library = this.libraries.get(game.libraryId);
if (!library) return undefined;

await this.lockVersion(gameId, versionName);

taskHandler.create({
id: taskId,
taskGroup: "import:game",
Expand Down Expand Up @@ -387,9 +325,6 @@ class LibraryManager {

progress(100);
},
async finally() {
await libraryManager.unlockVersion(gameId, versionName);
},
});

return taskId;
Expand All @@ -403,7 +338,7 @@ class LibraryManager {
) {
const library = this.libraries.get(libraryId);
if (!library) return undefined;
return library.peekFile(game, version, filename);
return await library.peekFile(game, version, filename);
}

async readFile(
Expand All @@ -415,7 +350,7 @@ class LibraryManager {
) {
const library = this.libraries.get(libraryId);
if (!library) return undefined;
return library.readFile(game, version, filename, options);
return await library.readFile(game, version, filename, options);
}
}

Expand Down
Loading