diff --git a/package-lock.json b/package-lock.json index 87d0d0b7..41809e42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "fs-extra": "^11.1.1", "history": "^5.3.0", "is-online": "^9.0.1", + "jszip": "^3.10.1", "md5-file": "^5.0.0", "node-fetch": "^2.6.7", "node-stream-zip": "^1.15.0", @@ -9872,8 +9873,7 @@ "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "dev": true + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" }, "node_modules/immutable": { "version": "4.1.0", @@ -12158,7 +12158,6 @@ "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dev": true, "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", @@ -12170,7 +12169,6 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -12297,7 +12295,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, "dependencies": { "immediate": "~3.0.5" } @@ -13831,8 +13828,7 @@ "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "node_modules/param-case": { "version": "3.0.4", @@ -16213,8 +16209,7 @@ "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, "node_modules/setprototypeof": { "version": "1.2.0", @@ -26235,8 +26230,7 @@ "immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "dev": true + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" }, "immutable": { "version": "4.1.0", @@ -28053,7 +28047,6 @@ "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dev": true, "requires": { "lie": "~3.3.0", "pako": "~1.0.2", @@ -28065,7 +28058,6 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -28178,7 +28170,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, "requires": { "immediate": "~3.0.5" } @@ -29308,8 +29299,7 @@ "pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "param-case": { "version": "3.0.4", @@ -31014,8 +31004,7 @@ "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, "setprototypeof": { "version": "1.2.0", diff --git a/package.json b/package.json index 8dbbed7e..7b664e8d 100644 --- a/package.json +++ b/package.json @@ -244,6 +244,7 @@ "fs-extra": "^11.1.1", "history": "^5.3.0", "is-online": "^9.0.1", + "jszip": "^3.10.1", "md5-file": "^5.0.0", "node-fetch": "^2.6.7", "node-stream-zip": "^1.15.0", diff --git a/src/main/helpers/zip.helpers.ts b/src/main/helpers/zip.helpers.ts new file mode 100644 index 00000000..d04fd794 --- /dev/null +++ b/src/main/helpers/zip.helpers.ts @@ -0,0 +1,23 @@ +import JSZip from 'jszip'; +import { pathExist } from './fs.helpers'; +import path from 'path'; +import { mkdir, writeFile } from 'fs/promises'; + +export async function extractZip(zip: JSZip, dest: string): Promise{ + if(!await pathExist(dest)){ throw new Error(`Path ${dest} does not exist`); } + const files: string[] = []; + await zip.forEach(async (relativePath, entry) => { + if(entry.dir){ return; } + + const content = await entry.async("nodebuffer"); + const outPath = path.join(dest, relativePath); + const outDir = path.dirname(outPath); + + await mkdir(outDir, { recursive: true }); + await writeFile(outPath, content); + + files.push(outPath); + }); + + return files; +} \ No newline at end of file diff --git a/src/main/services/mods/bs-mods-manager.service.ts b/src/main/services/mods/bs-mods-manager.service.ts index 74b2d535..55e8259b 100644 --- a/src/main/services/mods/bs-mods-manager.service.ts +++ b/src/main/services/mods/bs-mods-manager.service.ts @@ -6,13 +6,15 @@ 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"; import { spawn } from "child_process"; import { BS_EXECUTABLE } from "../../constants"; import log from "electron-log"; -import { deleteFolder, ensureFolderExist, pathExist, unlinkPath } from "../../helpers/fs.helpers"; +import { deleteFolder, pathExist, unlinkPath } from "../../helpers/fs.helpers"; import { lastValueFrom } from "rxjs"; +import JSZip from "jszip" +import { extractZip } from "../../helpers/zip.helpers"; +import { ensureFolderExist } from "../../helpers/fs.helpers"; export class BsModsManagerService { @@ -96,17 +98,20 @@ export class BsModsManagerService { return this.getIpaFromHash(injectorMd5); } - private async downloadZip(zipUrl: string): Promise<{zip: StreamZip.StreamZipAsync, zipPath: string}>{ + private async downloadZip(zipUrl: string): Promise{ zipUrl = path.join(this.beatModsApi.BEAT_MODS_URL, zipUrl); - const fileName = path.basename(zipUrl); - const tempPath = this.utilsService.getTempPath(); - await ensureFolderExist(this.utilsService.getTempPath()); - const dest = path.join(tempPath, fileName); - const zipPath = (await lastValueFrom(this.requestService.downloadFile(zipUrl, dest))).data; - const zip = new StreamZip.async({file : zipPath}); + const buffer = await lastValueFrom(this.requestService.downloadBuffer(zipUrl)).then(progress => progress.data).catch(e => { + log.error("ZIP", "Error while downloading zip", e); + return undefined + }); - return {zip, zipPath}; + if(!buffer){ return null; } + + return JSZip.loadAsync(buffer).catch(e => { + log.error("ZIP", "Error while loading zip", e); + return null + }); } private async executeBSIPA(version: BSVersion, args: string[]): Promise{ @@ -135,23 +140,22 @@ export class BsModsManagerService { } private async installMod(mod: Mod, version: BSVersion): Promise{ - + this.utilsService.ipcSend("mod-installed", {success: true, data: {name: mod.name, progression: ((this.nbInstalledMods + 1) / this.nbModsToInstall) * 100}}) const download = this.getModDownload(mod, version); if(!download){ return false; } - const {zip, zipPath} = await this.downloadZip(download.url); + const zip = await this.downloadZip(download.url); if(!zip){ return false; } const crypto = require('crypto'); - const entries = await zip.entries(); + const files = await zip.files; - const checkedEntries = (await Promise.all(Object.values(entries).map(async (entry) => { - if(!entry.isFile){ return undefined; } - const data = await zip.entryData(entry); + const checkedEntries = (await Promise.all(Object.values(files).map(async (entry) => { + const data = await entry.async("nodebuffer"); const entryMd5 = crypto.createHash('md5').update(data).digest('hex') return download.hashMd5.some(md5 => md5.hash === entryMd5) ? entry : undefined; }))).filter(entry => !!entry); @@ -162,12 +166,16 @@ export class BsModsManagerService { const isBSIPA = mod.name.toLowerCase() === "bsipa"; const destDir = isBSIPA ? verionPath : path.join(verionPath, ModsInstallFolder.PENDING); - const extracted = await zip.extract(null, destDir).then(() => true).catch(err => {log.error(err); return false}); + await ensureFolderExist(destDir); + const extracted = await extractZip(zip, destDir).then(() => true).catch(e => { + log.error("EXTRACT MOD ZIP", e); + return false; + }) - await zip.close(); - await unlinkPath(zipPath); - - const res = isBSIPA ? (extracted && (await this.executeBSIPA(version, ["-n"]))) : extracted; + const res = isBSIPA ? (extracted && (await this.executeBSIPA(version, ["-n"]).catch(e => { + log.error(e); + return false; + }))) : extracted; res && this.nbInstalledMods++; diff --git a/src/main/services/request.service.ts b/src/main/services/request.service.ts index b7a2cf90..17089c76 100644 --- a/src/main/services/request.service.ts +++ b/src/main/services/request.service.ts @@ -61,17 +61,30 @@ export class RequestService { } - public downloadBuffer(url: string): Observable{ + public downloadBuffer(url: string): Observable>{ + + return new Observable>(subscriber => { + + const progress: Progression = { + current: 0, + total: 0, + data: null + }; - return new Observable(subscriber => { const allChunks: Buffer[] = []; const req = get(url, res => { + + progress.total = parseInt(res.headers?.["content-length"] || "0", 10); + res.on("data", chunk => { allChunks.push(chunk); + progress.current += chunk.length; + subscriber.next(progress); }); res.on('end', () => { - subscriber.next(Buffer.concat(allChunks)); + progress.data = Buffer.concat(allChunks); + subscriber.next(progress); subscriber.complete(); }); res.on('error', (err) => subscriber.error(err))