diff --git a/package.json b/package.json
index deb17af..20b30bf 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"postinstall": "nuxt prepare"
},
"dependencies": {
- "@drop/droplet": "^0.4.4",
+ "@drop/droplet": "^0.5.0",
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5",
"@prisma/client": "5.20.0",
@@ -28,7 +28,8 @@
"turndown": "^7.2.0",
"uuid": "^10.0.0",
"vue": "latest",
- "vue-router": "latest"
+ "vue-router": "latest",
+ "vuedraggable": "^4.1.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"devDependencies": {
@@ -46,7 +47,7 @@
"tailwindcss": "^3.4.13"
},
"optionalDependencies": {
- "@drop/droplet-linux-x64-gnu": "^0.4.4",
- "@drop/droplet-win32-x64-msvc": "^0.4.4"
+ "@drop/droplet-linux-x64-gnu": "^0.5.0",
+ "@drop/droplet-win32-x64-msvc": "^0.5.0"
}
}
diff --git a/pages/admin/library/[id]/import.vue b/pages/admin/library/[id]/import.vue
index 53bbe41..feaeea7 100644
--- a/pages/admin/library/[id]/import.vue
+++ b/pages/admin/library/[id]/import.vue
@@ -74,7 +74,7 @@
-
+
-
+
-
-
- Images
-
-
-
+
+ Images
+
+
+
+
+ Manage version order
+
+
lowest
+
updateVersionOrder()"
+ :list="game.versions"
+ handle=".handle"
+ class="mt-2 space-y-4"
+ >
+
+
+
+ {{ item.versionName }}
+
+
+ {{ item.delta ? "Upgrade mode" : "" }}
+
+
+
+
+
+
+
+
+
highest
+
{{ game.versions }}
+
diff --git a/plugins/vuedraggable.ts b/plugins/vuedraggable.ts
new file mode 100644
index 0000000..58d50a0
--- /dev/null
+++ b/plugins/vuedraggable.ts
@@ -0,0 +1,5 @@
+import draggable from "vuedraggable";
+
+export default defineNuxtPlugin((nuxtApp) => {
+ nuxtApp.vueApp.component("draggable", draggable);
+});
diff --git a/prisma/migrations/20241014052934_add_delta_and_order/migration.sql b/prisma/migrations/20241014052934_add_delta_and_order/migration.sql
new file mode 100644
index 0000000..9619d00
--- /dev/null
+++ b/prisma/migrations/20241014052934_add_delta_and_order/migration.sql
@@ -0,0 +1,9 @@
+/*
+ Warnings:
+
+ - Added the required column `versionIndex` to the `GameVersion` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE "GameVersion" ADD COLUMN "delta" BOOLEAN NOT NULL DEFAULT false,
+ADD COLUMN "versionIndex" INTEGER NOT NULL;
diff --git a/prisma/migrations/20241014053941_remove_version_order/migration.sql b/prisma/migrations/20241014053941_remove_version_order/migration.sql
new file mode 100644
index 0000000..8e05931
--- /dev/null
+++ b/prisma/migrations/20241014053941_remove_version_order/migration.sql
@@ -0,0 +1,8 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `versionOrder` on the `Game` table. All the data in the column will be lost.
+
+*/
+-- AlterTable
+ALTER TABLE "Game" DROP COLUMN "versionOrder";
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 9eb6e7b..fefe74b 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -91,7 +91,6 @@ model Game {
mCoverId String
mImageLibrary String[] // linked to objects in s3
- versionOrder String[]
versions GameVersion[]
libraryBasePath String @unique // Base dir for all the game versions
@@ -109,6 +108,9 @@ model GameVersion {
setupCommand String // Command to setup game (dependencies and such)
dropletManifest Json // Results from droplet
+ versionIndex Int
+ delta Boolean @default(false)
+
@@id([gameId, versionName])
}
diff --git a/server/api/v1/admin/game/index.get.ts b/server/api/v1/admin/game/index.get.ts
index 4afbd2e..06c7984 100644
--- a/server/api/v1/admin/game/index.get.ts
+++ b/server/api/v1/admin/game/index.get.ts
@@ -16,6 +16,19 @@ export default defineEventHandler(async (h3) => {
where: {
id: gameId,
},
+ include: {
+ versions: {
+ orderBy: {
+ versionIndex: "asc",
+ },
+ select: {
+ versionIndex: true,
+ versionName: true,
+ platform: true,
+ delta: true,
+ }
+ },
+ },
});
if (!game)
diff --git a/server/api/v1/admin/game/version.delete.ts b/server/api/v1/admin/game/version.delete.ts
new file mode 100644
index 0000000..a80da87
--- /dev/null
+++ b/server/api/v1/admin/game/version.delete.ts
@@ -0,0 +1,26 @@
+import prisma from "~/server/internal/db/database";
+
+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 gameId = body.id.toString();
+ const version = body.versionName.toString();
+ if (!gameId || !version)
+ throw createError({
+ statusCode: 400,
+ statusMessage: "Missing ID or versionName in body",
+ });
+
+ await prisma.gameVersion.delete({
+ where: {
+ gameId_versionName: {
+ gameId: gameId,
+ versionName: version,
+ },
+ },
+ });
+
+ return {};
+});
diff --git a/server/api/v1/admin/game/version.post.ts b/server/api/v1/admin/game/version.post.ts
new file mode 100644
index 0000000..bbb2fc3
--- /dev/null
+++ b/server/api/v1/admin/game/version.post.ts
@@ -0,0 +1,40 @@
+import prisma from "~/server/internal/db/database";
+
+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 gameId = body.id?.toString();
+ // We expect an array of the version names for this game
+ const versions: string[] | undefined = body.versions;
+ if (!gameId || !versions || !Array.isArray(versions))
+ throw createError({
+ statusCode: 400,
+ statusMessage: "Missing id, versions or versions is not an array",
+ });
+
+ const newVersions = await prisma.$transaction(
+ versions.map((versionName, versionIndex) =>
+ prisma.gameVersion.update({
+ where: {
+ gameId_versionName: {
+ gameId: gameId,
+ versionName: versionName,
+ },
+ },
+ data: {
+ versionIndex: versionIndex,
+ },
+ select: {
+ versionIndex: true,
+ versionName: true,
+ platform: true,
+ delta: true,
+ }
+ })
+ )
+ );
+
+ return newVersions;
+});
diff --git a/server/api/v1/admin/import/version/index.post.ts b/server/api/v1/admin/import/version/index.post.ts
index 162d4a5..a107ea6 100644
--- a/server/api/v1/admin/import/version/index.post.ts
+++ b/server/api/v1/admin/import/version/index.post.ts
@@ -10,23 +10,24 @@ export default defineEventHandler(async (h3) => {
const platform = body.platform;
const startup = body.startup;
const setup = body.setup ?? "";
- if (
- !gameId ||
- !versionName ||
- !platform ||
- !startup
- )
+ const delta = body.delta ?? false;
+ if (!gameId || !versionName || !platform || (!delta && !startup))
throw createError({
statusCode: 400,
statusMessage:
"Missing id, version, platform, setup or startup from body",
});
- const taskId = await libraryManager.importVersion(gameId, versionName, {
- platform,
- startup,
- setup,
- });
+ const taskId = await libraryManager.importVersion(
+ gameId,
+ versionName,
+ {
+ platform,
+ startup,
+ setup,
+ },
+ delta
+ );
if (!taskId)
throw createError({
statusCode: 400,
diff --git a/server/api/v1/client/metadata/endpoint.get.ts b/server/api/v1/client/metadata/endpoints.get.ts
similarity index 100%
rename from server/api/v1/client/metadata/endpoint.get.ts
rename to server/api/v1/client/metadata/endpoints.get.ts
diff --git a/server/api/v1/client/metadata/manifest.get.ts b/server/api/v1/client/metadata/manifest.get.ts
new file mode 100644
index 0000000..bc3df5b
--- /dev/null
+++ b/server/api/v1/client/metadata/manifest.get.ts
@@ -0,0 +1,12 @@
+import { defineClientEventHandler } from "~/server/internal/clients/event-handler";
+
+export default defineClientEventHandler(async (h3) => {
+ const query = getQuery(h3);
+ const id = query.id?.toString();
+ const version = query.version?.toString();
+ if (!id || !version)
+ throw createError({
+ statusCode: 400,
+ statusMessage: "Missing id or version in query",
+ });
+});
diff --git a/server/api/v1/client/metadata/versions.get.ts b/server/api/v1/client/metadata/versions.get.ts
index 5d49db0..d40423a 100644
--- a/server/api/v1/client/metadata/versions.get.ts
+++ b/server/api/v1/client/metadata/versions.get.ts
@@ -14,6 +14,14 @@ export default defineClientEventHandler(async (h3, {}) => {
where: {
gameId: id,
},
+ select: {
+ versionIndex: true,
+ versionName: true,
+ platform: true,
+ setupCommand: true,
+ launchCommand: true,
+ delta: true,
+ }
});
return versions;
diff --git a/server/internal/downloads/README.md b/server/internal/P2P.md
similarity index 54%
rename from server/internal/downloads/README.md
rename to server/internal/P2P.md
index f1251aa..341e345 100644
--- a/server/internal/downloads/README.md
+++ b/server/internal/P2P.md
@@ -1,8 +1,16 @@
-# Drop Download System
-Drop downloads come in two types:
+# Drop P2P System
+
+Drop clients have a variety of P2P or P2P-like methods of data transfer available
## Public (not quite) HTTPS downloads endpoints
+
These use public HTTPS certificate, and while are authenticated, are 'public' in the sense that they aren't P2P; anyone can connect to them
## Private mTLS P2P endpoints
-Drop clients use P2P mTLS aided by the P2P co-ordinator to transfer chunks between themselves.
\ No newline at end of file
+
+Drop clients use P2P mTLS aided by the P2P co-ordinator to transfer chunks between themselves. This happens over HTTP.
+
+
+## Private mTLS Wireguard tunnels
+
+Drop clients can establish P2P Wireguard
\ No newline at end of file
diff --git a/server/internal/downloads/manifest.ts b/server/internal/downloads/manifest.ts
new file mode 100644
index 0000000..d646d58
--- /dev/null
+++ b/server/internal/downloads/manifest.ts
@@ -0,0 +1,52 @@
+export type DropChunk = {
+ permissions: number;
+ ids: string[];
+ checksums: string[];
+ lengths: string[];
+};
+
+export type DropManifest = {
+ [key: string]: DropChunk;
+};
+
+export type DropManifestMetadata = {
+ manifest: DropManifest;
+ versionName: string;
+};
+
+export type DropGeneratedManifest = DropManifest & {
+ [key: string]: { versionName: string };
+};
+
+class ManifestGenerator {
+ static generateManifest(
+ rootManifest: DropManifestMetadata,
+ ...overlays: DropManifestMetadata[]
+ ): DropGeneratedManifest {
+ if (overlays.length == 0) {
+ return Object.fromEntries(
+ Object.entries(rootManifest.manifest).map(([key, value]) => [
+ key,
+ Object.assign({}, value, { versionName: rootManifest.versionName }),
+ ])
+ );
+ }
+
+ // Recurse in verse order through versions, skipping files that already exist.
+ const versions = [...overlays.reverse(), rootManifest];
+ const manifest: DropGeneratedManifest = {};
+ for (const version of versions) {
+ for (const [filename, chunk] of Object.entries(version.manifest)) {
+ if (manifest[filename]) continue;
+ manifest[filename] = Object.assign({}, chunk, {
+ versionName: version.versionName,
+ });
+ }
+ }
+
+ return manifest;
+ }
+}
+
+export const manifestGenerator = new ManifestGenerator();
+export default manifestGenerator;
diff --git a/server/internal/library/index.ts b/server/internal/library/index.ts
index 70eddbb..8f1878b 100644
--- a/server/internal/library/index.ts
+++ b/server/internal/library/index.ts
@@ -195,7 +195,8 @@ class LibraryManager {
async importVersion(
gameId: string,
versionName: string,
- metadata: { platform: string; setup: string; startup: string }
+ metadata: { platform: string; setup: string; startup: string },
+ delta = false
) {
const taskId = `import:${gameId}:${versionName}`;
@@ -238,6 +239,10 @@ class LibraryManager {
log("Created manifest successfully!");
+ const currentIndex = await prisma.gameVersion.count({
+ where: { gameId: gameId },
+ });
+
// Then, create the database object
const version = await prisma.gameVersion.create({
data: {
@@ -247,6 +252,8 @@ class LibraryManager {
setupCommand: metadata.setup,
launchCommand: metadata.startup,
dropletManifest: manifest,
+ versionIndex: currentIndex,
+ delta: delta,
},
});
diff --git a/yarn.lock b/yarn.lock
index c3d4b0d..6bc5dd5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -296,23 +296,23 @@
dependencies:
mime "^3.0.0"
-"@drop/droplet-linux-x64-gnu@0.4.4", "@drop/droplet-linux-x64-gnu@^0.4.4":
- version "0.4.4"
- resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-linux-x64-gnu/-/@drop/droplet-linux-x64-gnu-0.4.4.tgz#6678a0923bb13d37e20cae467f45c72bc5d9fe6e"
- integrity sha1-ZnigkjuxPTfiDK5Gf0XHK8XZ/m4=
+"@drop/droplet-linux-x64-gnu@0.5.0", "@drop/droplet-linux-x64-gnu@^0.5.0":
+ version "0.5.0"
+ resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-linux-x64-gnu/-/@drop/droplet-linux-x64-gnu-0.5.0.tgz#06643f7bc79de4b35a395295ef1e29fad46f32b5"
+ integrity sha1-BmQ/e8ed5LNaOVKV7x4p+tRvMrU=
-"@drop/droplet-win32-x64-msvc@0.4.4", "@drop/droplet-win32-x64-msvc@^0.4.4":
- version "0.4.4"
- resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-win32-x64-msvc/-/@drop/droplet-win32-x64-msvc-0.4.4.tgz#10802bb36c6ec7d69aa17ea22081e5d5f0dac3c3"
- integrity sha1-EIArs2xux9aaoX6iIIHl1fDaw8M=
+"@drop/droplet-win32-x64-msvc@0.5.0", "@drop/droplet-win32-x64-msvc@^0.5.0":
+ version "0.5.0"
+ resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-win32-x64-msvc/-/@drop/droplet-win32-x64-msvc-0.5.0.tgz#abc02af2102f0faaf4561473b7a18395a0ba2b10"
+ integrity sha1-q8Aq8hAvD6r0VhRzt6GDlaC6KxA=
-"@drop/droplet@^0.4.4":
- version "0.4.4"
- resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet/-/@drop/droplet-0.4.4.tgz#a9b6e3a341e85703b25c7fee597261e1b239a280"
- integrity sha1-qbbjo0HoVwOyXH/uWXJh4bI5ooA=
+"@drop/droplet@^0.5.0":
+ version "0.5.0"
+ resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet/-/@drop/droplet-0.5.0.tgz#27da4f7292c9b860d38bb785c0fb2bb3b9cd50a3"
+ integrity sha1-J9pPcpLJuGDTi7eFwPsrs7nNUKM=
optionalDependencies:
- "@drop/droplet-linux-x64-gnu" "0.4.4"
- "@drop/droplet-win32-x64-msvc" "0.4.4"
+ "@drop/droplet-linux-x64-gnu" "0.5.0"
+ "@drop/droplet-win32-x64-msvc" "0.5.0"
"@esbuild/aix-ppc64@0.20.2":
version "0.20.2"
@@ -4887,6 +4887,11 @@ smob@^1.0.0:
resolved "https://registry.yarnpkg.com/smob/-/smob-1.5.0.tgz#85d79a1403abf128d24d3ebc1cdc5e1a9548d3ab"
integrity sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==
+sortablejs@1.14.0:
+ version "1.14.0"
+ resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.14.0.tgz#6d2e17ccbdb25f464734df621d4f35d4ab35b3d8"
+ integrity sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==
+
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.2.0, source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
@@ -5656,6 +5661,13 @@ vue@^3.5.5, vue@latest:
"@vue/server-renderer" "3.5.10"
"@vue/shared" "3.5.10"
+vuedraggable@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-4.1.0.tgz#edece68adb8a4d9e06accff9dfc9040e66852270"
+ integrity sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==
+ dependencies:
+ sortablejs "1.14.0"
+
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"