version importing
This commit is contained in:
@@ -11,6 +11,9 @@ import prisma from "../db/database";
|
||||
import { GameVersion, Platform } from "@prisma/client";
|
||||
import { fuzzy } from "fast-fuzzy";
|
||||
import { recursivelyReaddir } from "../utils/recursivedirs";
|
||||
import taskHandler from "../tasks";
|
||||
import { parsePlatform } from "../utils/parseplatform";
|
||||
import droplet from "@drop/droplet";
|
||||
|
||||
class LibraryManager {
|
||||
private basePath: string;
|
||||
@@ -143,7 +146,7 @@ class LibraryManager {
|
||||
match: number;
|
||||
}> = [];
|
||||
|
||||
const files = recursivelyReaddir(targetDir);
|
||||
const files = recursivelyReaddir(targetDir, 2);
|
||||
for (const file of files) {
|
||||
const filename = path.basename(file);
|
||||
const dotLocation = file.lastIndexOf(".");
|
||||
@@ -188,6 +191,73 @@ class LibraryManager {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async importVersion(
|
||||
gameId: string,
|
||||
versionName: string,
|
||||
metadata: { platform: string; setup: string; startup: string }
|
||||
) {
|
||||
const taskId = `import:${gameId}:${versionName}`;
|
||||
|
||||
const platform = parsePlatform(metadata.platform);
|
||||
if (!platform) return undefined;
|
||||
|
||||
const game = await prisma.game.findUnique({
|
||||
where: { id: gameId },
|
||||
select: { mName: true, libraryBasePath: true },
|
||||
});
|
||||
if (!game) return undefined;
|
||||
|
||||
const baseDir = path.join(this.basePath, game.libraryBasePath, versionName);
|
||||
if (!fs.existsSync(baseDir)) return undefined;
|
||||
|
||||
taskHandler.create({
|
||||
id: taskId,
|
||||
name: `Importing version ${versionName} for ${game.mName}`,
|
||||
requireAdmin: true,
|
||||
async run({ progress, log }) {
|
||||
// First, create the manifest via droplet.
|
||||
// This takes up 90% of our progress, so we wrap it in a *0.9
|
||||
const manifest = await new Promise<string>((resolve, reject) => {
|
||||
droplet.generateManifest(
|
||||
baseDir,
|
||||
(err, value) => {
|
||||
if (err) return reject(err);
|
||||
progress(value * 0.9);
|
||||
},
|
||||
(err, line) => {
|
||||
if (err) return reject(err);
|
||||
log(line);
|
||||
},
|
||||
(err, manifest) => {
|
||||
if (err) return reject(err);
|
||||
resolve(manifest);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
log("Created manifest successfully!");
|
||||
|
||||
// Then, create the database object
|
||||
const version = await prisma.gameVersion.create({
|
||||
data: {
|
||||
gameId: gameId,
|
||||
versionName: versionName,
|
||||
platform: platform,
|
||||
setupCommand: metadata.setup,
|
||||
launchCommand: metadata.startup,
|
||||
dropletManifest: manifest,
|
||||
},
|
||||
});
|
||||
|
||||
log("Successfully created version!");
|
||||
|
||||
progress(100);
|
||||
},
|
||||
});
|
||||
|
||||
return taskId;
|
||||
}
|
||||
}
|
||||
|
||||
export const libraryManager = new LibraryManager();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import droplet from "@drop/droplet";
|
||||
|
||||
/**
|
||||
* The TaskHandler setups up two-way connections to web clients and manages the state for them
|
||||
* This allows long-running tasks (like game imports and such) to report progress, success and error states
|
||||
@@ -5,18 +7,19 @@
|
||||
*/
|
||||
|
||||
type TaskRegistryEntry = {
|
||||
runPromise: Promise<void>;
|
||||
success: boolean;
|
||||
progress: number;
|
||||
log: string[];
|
||||
error: string | undefined;
|
||||
clients: { [key: string]: boolean };
|
||||
name: string;
|
||||
requireAdmin: boolean;
|
||||
};
|
||||
|
||||
class TaskHandler {
|
||||
private taskRegistry: { [key: string]: TaskRegistryEntry } = {};
|
||||
private clientRegistry: { [key: string]: PeerImpl } = {};
|
||||
startTasks: (() => void)[] = [];
|
||||
|
||||
constructor() {}
|
||||
|
||||
@@ -30,17 +33,18 @@ class TaskHandler {
|
||||
if (!taskEntry) return;
|
||||
const taskMessage: TaskMessage = {
|
||||
id: task.id,
|
||||
name: task.name,
|
||||
success: taskEntry.success,
|
||||
progress: taskEntry.progress,
|
||||
error: taskEntry.error,
|
||||
log: taskEntry.log,
|
||||
log: taskEntry.log.reverse().slice(0, 50),
|
||||
};
|
||||
for (const client of Object.keys(taskEntry.clients)) {
|
||||
if (!this.clientRegistry[client]) continue;
|
||||
this.clientRegistry[client].send(taskMessage);
|
||||
}
|
||||
updateCollectTimeout = undefined;
|
||||
}, 500);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const progress = (progress: number) => {
|
||||
@@ -57,40 +61,46 @@ class TaskHandler {
|
||||
updateAllClients();
|
||||
};
|
||||
|
||||
const promiseRun = task.run({ progress, log });
|
||||
promiseRun.then(() => {
|
||||
const taskEntry = this.taskRegistry[task.id];
|
||||
if (!taskEntry) return;
|
||||
this.taskRegistry[task.id].success = true;
|
||||
updateAllClients();
|
||||
});
|
||||
promiseRun.catch((error) => {
|
||||
const taskEntry = this.taskRegistry[task.id];
|
||||
if (!taskEntry) return;
|
||||
this.taskRegistry[task.id].success = false;
|
||||
this.taskRegistry[task.id].error = error;
|
||||
updateAllClients();
|
||||
});
|
||||
this.taskRegistry[task.id] = {
|
||||
name: task.name,
|
||||
runPromise: promiseRun,
|
||||
success: false,
|
||||
progress: 0,
|
||||
error: undefined,
|
||||
log: [],
|
||||
clients: {},
|
||||
requireAdmin: task.requireAdmin ?? false,
|
||||
};
|
||||
|
||||
droplet.callAltThreadFunc(async () => {
|
||||
const promiseRun = task.run({ progress, log });
|
||||
promiseRun.then(() => {
|
||||
const taskEntry = this.taskRegistry[task.id];
|
||||
if (!taskEntry) return;
|
||||
this.taskRegistry[task.id].success = true;
|
||||
updateAllClients();
|
||||
});
|
||||
promiseRun.catch((error) => {
|
||||
const taskEntry = this.taskRegistry[task.id];
|
||||
if (!taskEntry) return;
|
||||
this.taskRegistry[task.id].success = false;
|
||||
this.taskRegistry[task.id].error = error;
|
||||
updateAllClients();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
connect(id: string, taskId: string, peer: PeerImpl) {
|
||||
connect(id: string, taskId: string, peer: PeerImpl, isAdmin = false) {
|
||||
const task = this.taskRegistry[taskId];
|
||||
if (!task) return false;
|
||||
|
||||
if (task.requireAdmin && !isAdmin) return false;
|
||||
|
||||
this.clientRegistry[id] = peer;
|
||||
this.taskRegistry[taskId].clients[id] = true; // Uniquely insert client to avoid sending duplicate traffic
|
||||
|
||||
const catchupMessage: TaskMessage = {
|
||||
id: taskId,
|
||||
name: task.name,
|
||||
success: task.success,
|
||||
error: task.error,
|
||||
log: task.log,
|
||||
@@ -127,10 +137,12 @@ export interface Task {
|
||||
id: string;
|
||||
name: string;
|
||||
run: (context: TaskRunContext) => Promise<void>;
|
||||
requireAdmin?: boolean;
|
||||
}
|
||||
|
||||
export type TaskMessage = {
|
||||
id: string;
|
||||
name: string;
|
||||
success: boolean;
|
||||
progress: number;
|
||||
error: undefined | string;
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export function recursivelyReaddir(dir: string) {
|
||||
export function recursivelyReaddir(dir: string, depth: number = 100) {
|
||||
if (depth == 0) return [];
|
||||
const result: Array<string> = [];
|
||||
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 subdirs = recursivelyReaddir(targetDir, depth - 1);
|
||||
const subdirsWithBase = subdirs.map((e) => path.join(dir, e));
|
||||
result.push(...subdirsWithBase);
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user