From a43e2389051e4b7a148e626cc8acdbf9f4138e3c Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Sat, 1 Oct 2022 02:56:15 +0200 Subject: [PATCH] we can now download and install mods from the back-end --- package-lock.json | 5 ++ package.json | 1 + release/app/package.json | 2 +- src/main/services/bs-local-version.service.ts | 13 ++-- .../services/mods/beat-mods-api.service.ts | 3 +- .../services/mods/bs-mods-manager.service.ts | 64 ++++++++++++++++++- src/main/services/request.service.ts | 19 +++++- src/main/services/utils.service.ts | 3 +- src/shared/models/mods/mod.interface.ts | 4 +- 9 files changed, 102 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 56ec7e6f..2bc7527d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9394,6 +9394,11 @@ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", "dev": true }, + "node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==" + }, "nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", diff --git a/package.json b/package.json index f31af38c..90263965 100644 --- a/package.json +++ b/package.json @@ -233,6 +233,7 @@ "history": "^5.3.0", "is-online": "^9.0.1", "md5-file": "^5.0.0", + "node-stream-zip": "^1.15.0", "react": "^18.2.0", "react-colorful": "^5.6.1", "react-dom": "^18.2.0", diff --git a/release/app/package.json b/release/app/package.json index 06d9fdc1..edbbdca7 100644 --- a/release/app/package.json +++ b/release/app/package.json @@ -1,5 +1,5 @@ { - "name": "BSManager", + "name": "bs-manager", "version": "0.1.0", "description": "A foundation for scalable desktop apps", "main": "./dist/main/main.js", diff --git a/src/main/services/bs-local-version.service.ts b/src/main/services/bs-local-version.service.ts index 73cb8b10..5b547789 100644 --- a/src/main/services/bs-local-version.service.ts +++ b/src/main/services/bs-local-version.service.ts @@ -90,11 +90,16 @@ export class BSLocalVersionService{ return seq.replace( /[<>:"\/\\|?*]+/g, '' ); } - public getVersionFolder(version: BSVersion){ - return version.name ? `${version.BSVersion}-${version.name}` : version.BSVersion; - } + public getVersionFolder(version: BSVersion){ + return version.name ? `${version.BSVersion}-${version.name}` : version.BSVersion; + } - public async getInstalledVersions(): Promise{ + public getVersionType(version: BSVersion): "steam"|"universal"{ + if(version.steam){ return "steam"; } + return "universal" + } + + public async getInstalledVersions(): Promise{ const versions: BSVersion[] = []; const steamBsFolder = await this.steamService.getGameFolder(BS_APP_ID, "Beat Saber") if(steamBsFolder && this.utilsService.pathExist(steamBsFolder)){ diff --git a/src/main/services/mods/beat-mods-api.service.ts b/src/main/services/mods/beat-mods-api.service.ts index 12b00584..de9dcea9 100644 --- a/src/main/services/mods/beat-mods-api.service.ts +++ b/src/main/services/mods/beat-mods-api.service.ts @@ -6,12 +6,13 @@ export class BeatModsApiService { private static instance: BeatModsApiService; - private readonly requestService: RequestService + private readonly requestService: RequestService; private readonly BEAT_MODS_VERSIONS = "https://versions.beatmods.com/versions.json"; private readonly BEAT_MODS_ALIAS = "https://alias.beatmods.com/aliases.json"; private readonly BEAT_MODS_API_URL = "https://beatmods.com/api/v1/"; + public readonly BEAT_MODS_URL = "https://beatmods.com"; private readonly aliasesCache = new Map(); private readonly versionModsCache = new Map(); diff --git a/src/main/services/mods/bs-mods-manager.service.ts b/src/main/services/mods/bs-mods-manager.service.ts index 7c7b76da..c8b5c0c7 100644 --- a/src/main/services/mods/bs-mods-manager.service.ts +++ b/src/main/services/mods/bs-mods-manager.service.ts @@ -5,14 +5,17 @@ import path from "path"; import { UtilsService } from "../utils.service"; import md5File from "md5-file"; import fs from "fs" +import StreamZip from "node-stream-zip"; +import { RequestService } from "../request.service"; export class BsModsManagerService { private static instance: BsModsManagerService; - private beatModsApi: BeatModsApiService; - private bsLocalService: BSLocalVersionService; - private utilsService: UtilsService; + private readonly beatModsApi: BeatModsApiService; + private readonly bsLocalService: BSLocalVersionService; + private readonly utilsService: UtilsService; + private readonly requestService: RequestService private manifestMatches: Mod[]; @@ -25,6 +28,7 @@ export class BsModsManagerService { this.beatModsApi = BeatModsApiService.getInstance(); this.bsLocalService = BSLocalVersionService.getInstance(); this.utilsService = UtilsService.getInstance(); + this.requestService = RequestService.getInstance(); } private async getModFromHash(hash: string): Promise{ @@ -81,6 +85,59 @@ export class BsModsManagerService { return this.getIpaFromHash(injectorMd5); } + private downloadZip(zipUrl: string): Promise{ + zipUrl = path.join(this.beatModsApi.BEAT_MODS_URL, zipUrl); + const fileName = path.basename(zipUrl); + const tempPath = this.utilsService.getTempPath(); + this.utilsService.createFolderIfNotExist(this.utilsService.getTempPath()); + const dest = path.join(tempPath, fileName); + return this.requestService.downloadFile(zipUrl, dest).then(zipPath => new StreamZip.async({file : zipPath})); + } + + private executeBSIPA(version: BSVersion){ + + } + + private installBSIPA(mod: Mod, version: BSVersion){ + this.installMod(mod, version).then(res => { + this.executeBSIPA(version); //TODO + }) + } + + public async installMod(mod: Mod, version: BSVersion): Promise{ + const download = mod.downloads.find(download => { + const type = download.type.toLowerCase() + return type === "universal" || type === this.bsLocalService.getVersionType(version); + }); + + if(!download){ return false; } + + const zip = await this.downloadZip(download.url); + + if(!zip){ return false; } + + const crypto = require('crypto'); + const entries = await zip.entries(); + + const checkedEntries = (await Promise.all(Object.values(entries).map(async (entry) => { + if(!entry.isFile){ return undefined; } + const data = await zip.entryData(entry); + const entryMd5 = crypto.createHash('md5').update(data).digest('hex') + return download.hashMd5.some(md5 => md5.hash === entryMd5) ? entry : undefined; + }))).filter(entry => !!entry); + + if(checkedEntries.length != download.hashMd5.length){ return false; } + + const verionPath = await this.bsLocalService.getVersionPath(version); + if(!this.utilsService.pathExist(verionPath)){ return false; } + + const destDir = mod.name.toLowerCase() === "bsipa" ? verionPath : path.join(verionPath, ModsInstallFolder.PENDING); + + await zip.extract(null, destDir); + + return true; + } + public getAvailableMods(version: BSVersion): Promise{ return this.beatModsApi.getVersionMods(version); } @@ -116,6 +173,7 @@ export class BsModsManagerService { const enum ModsInstallFolder { PLUGINS = "Plugins", LIBS = "Libs", + PENDING = "IPA/Pending", PLUGINS_PENDING = "IPA/Pending/Plugins", LIBS_PENDING = "IPA/Pending/Libs" } \ No newline at end of file diff --git a/src/main/services/request.service.ts b/src/main/services/request.service.ts index 89df50cd..03ce76fc 100644 --- a/src/main/services/request.service.ts +++ b/src/main/services/request.service.ts @@ -1,4 +1,5 @@ import { get } from "https"; +import { createWriteStream, unlink } from "fs"; export class RequestService { @@ -22,6 +23,22 @@ export class RequestService { res.on('error', (err) => reject(err)) }); }); - } + } + + public downloadFile(url: string, dest: string): Promise{ + return new Promise((resolve, reject) => { + const file = createWriteStream(dest); + get(url, res => { + res.pipe(file); + file.on("close", () => { + file.close(() => resolve(dest)) + }).on("error", err => { + unlink(dest, () => reject(err)); + }); + }).on("error", err => { + unlink(dest, () => reject(err)); + }); + }); + } } \ No newline at end of file diff --git a/src/main/services/utils.service.ts b/src/main/services/utils.service.ts index 280566a8..f5bae47f 100644 --- a/src/main/services/utils.service.ts +++ b/src/main/services/utils.service.ts @@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readdirSync, readFile } from "fs"; import { spawnSync } from "child_process"; import { homedir } from "os"; import path from "path"; -import { BrowserWindow } from "electron"; +import { app, BrowserWindow } from "electron"; import { rm } from "fs/promises"; import { IpcResponse } from "shared/models/ipc"; import log from "electron-log"; @@ -27,6 +27,7 @@ export class UtilsService{ public getAssetsScriptsPath(): string { return this.getAssetsPath("scripts") } public getAssestsJsonsPath(): string { return this.getAssetsPath("jsons"); } + public getTempPath(): string{ return path.join(app.getPath("temp"), app.getName()) } public setMainWindow(win: BrowserWindow){ this.mainWindow = win; } public getMainWindow(){ return this.mainWindow; } diff --git a/src/shared/models/mods/mod.interface.ts b/src/shared/models/mods/mod.interface.ts index 4a87c50a..8f325812 100644 --- a/src/shared/models/mods/mod.interface.ts +++ b/src/shared/models/mods/mod.interface.ts @@ -24,11 +24,13 @@ export interface ModAuthor { } export interface DownloadLink { - type: string, + type: DownloadLinkType, url: string, hashMd5: FileHashes[] } +export type DownloadLinkType = "universal"|"steam"|"oculus"; + export interface FileHashes { hash: string, file: string