From bfb6d28a345cb937cb7a016a44e5f70d4bfd159c Mon Sep 17 00:00:00 2001 From: silentrald Date: Thu, 14 Nov 2024 14:24:45 +0800 Subject: [PATCH] [bugfix] use YauzlZip class wrapper for extracting files * fixed issue where yauzl just closes after reading all files --- src/__tests__/unit/zip.test.ts | 108 +----- src/main/helpers/zip.helpers.ts | 331 ------------------ src/main/models/oculus-downloader.class.ts | 14 +- src/main/models/yauzl-zip.class.ts | 2 +- .../maps/local-maps-manager.service.ts | 126 ++++--- .../services/mods/bs-mods-manager.service.ts | 20 +- 6 files changed, 90 insertions(+), 511 deletions(-) delete mode 100644 src/main/helpers/zip.helpers.ts diff --git a/src/__tests__/unit/zip.test.ts b/src/__tests__/unit/zip.test.ts index 0eefaa16..4c228765 100644 --- a/src/__tests__/unit/zip.test.ts +++ b/src/__tests__/unit/zip.test.ts @@ -1,6 +1,5 @@ import path from "path"; import { mkdir, pathExistsSync, readFile, rm } from "fs-extra"; -import { extractZip, getFilesFromZip } from "main/helpers/zip.helpers"; import { YauzlZip } from "main/models/yauzl-zip.class"; const TEST_FOLDER = path.resolve(__dirname, "../../..", "assets", "tests"); @@ -10,99 +9,8 @@ const SUBFOLDERS_ZIP = path.join(TEST_FOLDER, "subfolders.zip"); const MANIFEST_ZIP = path.join(TEST_FOLDER, "manifest.zip"); const DESTINATION_FOLDER = path.join(TEST_FOLDER, "out"); -describe("Zip Server Service Test", () => { - beforeAll(async () => { - if (pathExistsSync(DESTINATION_FOLDER)) { - await rm(DESTINATION_FOLDER, { recursive: true, force: true }); - } - await mkdir(DESTINATION_FOLDER); - }); - - // Uses '/' - it("Extract standard zip", async () => { - await extractZip(STANDARD_ZIP, DESTINATION_FOLDER); - - for (const file of [ - "file_1.2.txt", - "file_1.3.txt", - ]) { - expect(pathExistsSync(path.join(DESTINATION_FOLDER, file))) - .toBe(true); - } - - const SUBFOLDER_PATH = path.join(DESTINATION_FOLDER, "folder_1.1"); - for (const file of [ - "file_2.1.txt", - "file_2.2.txt", - ]) { - expect(pathExistsSync(path.join(SUBFOLDER_PATH, file))) - .toBe(true); - } - }); - - // Uses '\\' - it("Extract map zips using forward slashes", async () => { - const beforeExtractedFolders: string[] = []; - const afterExtractedFolders: string[] = []; - await extractZip(WINDOWS_LEGACY_MAP_ZIP, DESTINATION_FOLDER, { - beforeFolderExtracted: (folder) => beforeExtractedFolders.push(folder), - afterFolderExtracted: (folder) => afterExtractedFolders.push(folder), - }); - - // Expect all the files to exists - for (const file of [ - "BPMInfo.dat", - "cover.jpg", - "ExpertLegacy.dat", - "ExpertPlusLawless.dat", - "ExpertPlusLegacy.dat", - "ExpertPlusStandard.dat", - "ExpertStandard.dat", - "Info.dat", - "song.egg", - ]) { - expect(pathExistsSync(path.join(DESTINATION_FOLDER, file))) - .toBe(true); - } - - const SUBFOLDER_PATH = path.join(DESTINATION_FOLDER, "pfp"); - for (const file of [ - "galaxyCompressed.jpg", - "gojiCompressed.jpg", - ]) { - expect(pathExistsSync(path.join(SUBFOLDER_PATH, file))) - .toBe(true); - } - - expect(beforeExtractedFolders).toContain("."); - expect(beforeExtractedFolders).toContain("pfp"); - expect(beforeExtractedFolders.length).toBe(2); - - expect(afterExtractedFolders).toContain("."); - expect(afterExtractedFolders).toContain("pfp"); - expect(afterExtractedFolders.length).toBe(2); - }); - - it("Extract manifest.json from zip file", async () => { - const zipBuffer = await readFile(MANIFEST_ZIP); - const buffers = await getFilesFromZip(zipBuffer, ["manifest.json"]); - const manifest = JSON.parse(buffers["manifest.json"].toString()); - expect(manifest).toBeTruthy(); - - // Check some of the fields if they are correct - expect(manifest.appId).toBe("some-app-id"); - expect(manifest.canonicalName).toBe("some-canonical-name"); - expect(manifest.isCore).toBe(true); - }); - - afterAll(async () => { - if (pathExistsSync(DESTINATION_FOLDER)) { - await rm(DESTINATION_FOLDER, { recursive: true, force: true }); - } - }); -}); - describe("Test YauzlZip class", () => { + let zip: YauzlZip; beforeAll(async () => { if (pathExistsSync(DESTINATION_FOLDER)) { @@ -111,11 +19,9 @@ describe("Test YauzlZip class", () => { await mkdir(DESTINATION_FOLDER); }); - - it("Extract standard zip", async () => { - const zip = await YauzlZip.fromPath(STANDARD_ZIP); + zip = await YauzlZip.fromPath(STANDARD_ZIP); const res = await zip.extract(DESTINATION_FOLDER); expect(res.sort()).toEqual([ @@ -147,7 +53,7 @@ describe("Test YauzlZip class", () => { it("Extract map zips using back slashes", async () => { - const zip = await YauzlZip.fromPath(WINDOWS_LEGACY_MAP_ZIP); + zip = await YauzlZip.fromPath(WINDOWS_LEGACY_MAP_ZIP); const res = await zip.extract(DESTINATION_FOLDER); expect(res.sort()).toEqual([ @@ -223,7 +129,7 @@ describe("Test YauzlZip class", () => { it("Read manifest.json from zip file", async () => { const zipBuffer = await readFile(MANIFEST_ZIP); - const zip = await YauzlZip.fromBuffer(zipBuffer); + zip = await YauzlZip.fromBuffer(zipBuffer); const buffer = await (await zip.findEntry(entry => entry.fileName === "manifest.json")).read(); const manifest = JSON.parse(buffer.toString()); expect(manifest).toBeTruthy(); @@ -234,7 +140,11 @@ describe("Test YauzlZip class", () => { expect(manifest.isCore).toBe(true); }); - + afterEach(() => { + if (zip) { + zip.close(); + } + }) afterAll(async () => { if (pathExistsSync(DESTINATION_FOLDER)) { diff --git a/src/main/helpers/zip.helpers.ts b/src/main/helpers/zip.helpers.ts deleted file mode 100644 index 0e09bcf8..00000000 --- a/src/main/helpers/zip.helpers.ts +++ /dev/null @@ -1,331 +0,0 @@ -import fs from "fs-extra"; -import path from "path"; -import yauzl from "yauzl"; -import { ensureFolderExist } from "./fs.helpers"; - -// NOTE: yauzl needs to be reopened once readEntry is done - -export interface ZipEntry { - name: string; - directory: boolean; - buffer?: Buffer; -}; - -export interface ZipExtractOptions { - // Stops the extraction immediately when set to true - terminate?: (entry: ZipEntry) => boolean; - // Whether or not to extract the file - condition?: (entry: ZipEntry) => boolean; - // Called before extracting the folder - beforeFolderExtracted?: (folder: string) => void; - // Called when the folder contents is fully extracted - afterFolderExtracted?: (folder: string) => void; -}; - -export interface ZipProcessOptions { - // Stops the extraction immediately when set to true - terminate?: (entry: ZipEntry) => boolean; - getBuffer?: boolean; -}; - -const YAUZL_OPTIONS: yauzl.Options = { - lazyEntries: true, - decodeStrings: true, -}; - -function openZip(data: string | Buffer, callback: (error: Error, zip: yauzl.ZipFile) => void) { - if (typeof data === "string") { - yauzl.open(data, YAUZL_OPTIONS, callback); - } else { - yauzl.fromBuffer(data, YAUZL_OPTIONS, callback); - } -} - -export async function extractZip( - data: string | Buffer, - destination: string, - options?: Readonly -): Promise { - await ensureFolderExist(destination); - - return new Promise((resolve, reject) => { - openZip(data, (openError: Error, zip: yauzl.ZipFile) => { - if (openError) return reject(openError); - handleExtractZip(zip, destination, options, resolve, reject); - }); - }); -} - -function handleExtractZip( - zip: yauzl.ZipFile, - destination: string, - options: Readonly | undefined, - resolve: (result: string[]) => void, reject: (error: Error) => void -) { - const files: string[] = []; - const zipEntry: ZipEntry = { - name: "", - directory: false, - }; - let indexes: number[] = []; - let dirname = ""; - - options?.beforeFolderExtracted?.("."); - const folderExtractEvents = options?.beforeFolderExtracted - || options?.afterFolderExtracted - ? (filename: string) => { - let newDirname = path.dirname(filename); - if (newDirname === ".") newDirname = ""; - - const comparison = compareStringIndex(dirname, newDirname); - if (comparison === -1) { return; } - - // Push after extraction - if (options?.afterFolderExtracted) { - for (let i = indexes.length - 1; i > -1 && indexes[i] > comparison; --i) { - options.afterFolderExtracted(dirname.substring(0, indexes[i])); - } - } - - // Push before extraction - indexes = getSlashIndexes(newDirname); - if (options?.beforeFolderExtracted) { - for (let i = 0; i < indexes.length; ++i) { - if (indexes[i] <= comparison) { continue; } - options.beforeFolderExtracted(newDirname.substring(0, indexes[i])); - } - } - - dirname = newDirname; - } : () => {}; - - zip.readEntry(); - - zip.on("entry", (entry: yauzl.Entry) => { - const absolutePath = path.join(destination, entry.fileName); - - // Entry is a directory / folder - if (entry.fileName.endsWith("/")) { - if (!fs.pathExistsSync(absolutePath)) { - fs.mkdirSync(absolutePath, { recursive: true }); - } - return zip.readEntry(); - } - - // Entry is a file - zip.openReadStream(entry, (readError, readStream) => { - if (readError) return reject(readError); - - folderExtractEvents(entry.fileName); - - zipEntry.name = entry.fileName; - zipEntry.directory = false; - zipEntry.buffer = undefined; - - if (options?.terminate?.(zipEntry)) { - readStream.destroy(); - zip.close(); - return resolve(files); - } - - if (options?.condition) { - if (!options.condition(zipEntry)) { - readStream.destroy(); - return zip.readEntry(); - } - } - - // For forward slashes since they don't create directories - const directory = path.dirname(absolutePath); - if (!fs.pathExistsSync(directory)) { - fs.mkdirSync(directory, { recursive: true }); - } - - const writeStream = fs.createWriteStream(absolutePath); - readStream.pipe(writeStream); - readStream.on("end", () => { - zip.readEntry() - }); - }); - }); - - zip.once("end", () => { - if (options?.afterFolderExtracted) { - // Push after extraction - for (let i = indexes.length - 1; i > -1; --i) { - options.afterFolderExtracted(dirname.substring(0, indexes[i])); - } - options.afterFolderExtracted("."); - } - - zip.close(); - resolve(files); - }); -} - -export function getFilesFromZip( - data: string | Buffer, - files: string[] -): Promise> { - return new Promise((resolve, reject) => { - openZip(data, (openError: Error, zip: yauzl.ZipFile) => { - if (openError) return reject(openError); - handleGetFilesFromZip(zip, files, resolve, reject); - }); - }); -} - -function handleGetFilesFromZip( - zip: yauzl.ZipFile, - files: string[], - resolve: (buffers: Record) => void, - reject: (error: Error) => void -) { - const buffers: Record = {}; - let count = 0; - - zip.readEntry(); - - zip.on("entry", (entry: yauzl.Entry) => { - // Entry is a directory / folder - if (entry.fileName.endsWith("/") || !files.includes(entry.fileName)) { - return zip.readEntry(); - } - - // Entry is in the files list - zip.openReadStream(entry, (readError, readStream) => { - if (readError) return reject(readError); - - const chunks: Buffer[] = []; - readStream.on("data", data => { - chunks.push(data); - }); - - readStream.on("end", () => { - buffers[entry.fileName] = Buffer.concat(chunks); - - ++count; - if (count === files.length) { - zip.close(); - return resolve(buffers); - } - - zip.readEntry(); - }); - }); - }); - - zip.once("end", () => { - zip.close(); - resolve(buffers); - }); -} - -// @params loop - this should not throw an error -export function processZip( - data: string | Buffer, - options: Readonly, - loop: (accumulator: T, entry: ZipEntry) => T, - initValue: T -): Promise { - return new Promise((resolve, reject) => { - openZip(data, (openError: Error, zip: yauzl.ZipFile) => { - if (openError) return reject(openError); - handleProcessZip( - zip, options, loop, initValue, - resolve, reject - ); - }); - }); -} - -function handleProcessZip( - zip: yauzl.ZipFile, - options: Readonly, - loop: (accumulator: T, entry: ZipEntry) => T, - accumulator: T, - resolve: (result: T) => void, reject: (error: Error) => void -): void { - const zipEntry: ZipEntry = { - name: "", - directory: false, - }; - - zip.readEntry(); - - zip.on("entry", (entry: yauzl.Entry) => { - const isDirectory = entry.fileName.endsWith("/"); - if (isDirectory || !options.getBuffer) { - zipEntry.name = entry.fileName; - zipEntry.directory = isDirectory; - zipEntry.buffer = undefined; - accumulator = loop(accumulator, zipEntry); - return zip.readEntry(); - } - - // Entry is a file - zip.openReadStream(entry, (readError, readStream) => { - if (readError) return reject(readError); - - zipEntry.name = entry.fileName; - zipEntry.directory = false; - zipEntry.buffer = undefined; - if (options?.terminate?.(zipEntry)) { - readStream.destroy(); - zip.close(); - return resolve(accumulator); - } - - const chunks: Buffer[] = []; - readStream.on("data", data => { - chunks.push(data); - }); - - readStream.on("end", () => { - zipEntry.buffer = Buffer.concat(chunks); - accumulator = loop(accumulator, zipEntry); - zip.readEntry(); - }); - }); - }); - - zip.once("end", () => { - zip.close(); - resolve(accumulator); - }); -} - -/** - * @returns(number) - * -1 = string are the same - * > 0 = index where they don't equal the same - */ -function compareStringIndex(str1: string, str2: string) { - const N = Math.min(str1.length, str2.length); - let i = 0; - for (; i < N; ++i) { - if (str1[i] !== str2[i]) { break; } - } - - return i === str1.length && i === str2.length - ? -1 : i; -} - -/** - * @returns(number[]) - int array of the positions of the slashes - */ -function getSlashIndexes(str: string) { - if (str === "") { - return []; - } - - const indexes: number[] = []; - for (let i = 0; i < str.length; ++i) { - if (str[i] === "/") { - indexes.push(i); - } - } - indexes.push(str.length); - return indexes; -} - diff --git a/src/main/models/oculus-downloader.class.ts b/src/main/models/oculus-downloader.class.ts index 50fba9e2..0b48e6fd 100644 --- a/src/main/models/oculus-downloader.class.ts +++ b/src/main/models/oculus-downloader.class.ts @@ -6,7 +6,7 @@ import { inflate } from "pako" import { EMPTY, Observable, ReplaySubject, Subscriber, catchError, filter, from, lastValueFrom, mergeMap, scan, share, tap } from "rxjs"; import { Progression, hashFile } from "../helpers/fs.helpers"; import { OculusDownloaderErrorCodes } from "../../shared/models/bs-version-download/oculus-download.model"; -import { getFilesFromZip } from "main/helpers/zip.helpers"; +import { YauzlZip } from "./yauzl-zip.class"; export class OculusDownloader { @@ -35,14 +35,18 @@ export class OculusDownloader { .catch(err => CustomError.throw(err, "DOWNLOAD_MANIFEST_FAILED")); const manifestName = "manifest.json"; - const buffers = await getFilesFromZip(buffer, [manifestName]); - const manifest = buffers[manifestName]; - if(!manifest) { + const zip = await YauzlZip.fromBuffer(buffer); + const entry = await zip.findEntry((entry) => entry.fileName === manifestName); + if(!entry) { throw new CustomError("Manifest file not found", "MANIFEST_FILE_NOT_FOUND"); } + const manifest = await entry.read(); return JSON.parse(manifest.toString()) - .catch((err: Error) => CustomError.throw(err, "PARSE_MANIFEST_FILE_FAILED")); + .catch((err: Error) => CustomError.throw(err, "PARSE_MANIFEST_FILE_FAILED")) + .finally(() => { + zip.close(); + }); } private downloadManifestFile(file: OculusManifestFile, destination: string): Observable> { diff --git a/src/main/models/yauzl-zip.class.ts b/src/main/models/yauzl-zip.class.ts index f2c4cc31..41b70ec5 100644 --- a/src/main/models/yauzl-zip.class.ts +++ b/src/main/models/yauzl-zip.class.ts @@ -7,6 +7,7 @@ export class YauzlZip { private static readonly YAUZL_OPEN_OPTIONS: Options = { lazyEntries: true, decodeStrings: true, + autoClose: false, }; public static fromPath(path: string): Promise { @@ -36,7 +37,6 @@ export class YauzlZip { private async readEntry(): Promise { return new Promise((resolve, reject) => { - const onEntry = (entry: Entry) => { cleanup(); resolve(new YauzlZipEntry({ entry, zip: this.zip })); diff --git a/src/main/services/additional-content/maps/local-maps-manager.service.ts b/src/main/services/additional-content/maps/local-maps-manager.service.ts index 8ff0d164..a353c114 100644 --- a/src/main/services/additional-content/maps/local-maps-manager.service.ts +++ b/src/main/services/additional-content/maps/local-maps-manager.service.ts @@ -13,7 +13,7 @@ import sanitize from "sanitize-filename"; import { DeepLinkService } from "../../deep-link.service"; import log from "electron-log"; import { WindowManagerService } from "../../window-manager.service"; -import { Observable, Subject, lastValueFrom, tap } from "rxjs"; +import { Observable, Subject, lastValueFrom } from "rxjs"; import { Archive } from "../../../models/archive.class"; import { Progression, deleteFolder, ensureFolderExist, getFilesInFolder, getFoldersInFolder, pathExist } from "../../../helpers/fs.helpers"; import { readFile } from "fs/promises"; @@ -29,7 +29,7 @@ import { MapInfo } from "shared/models/maps/info/map-info.model"; import { parseMapInfoDat } from "shared/parsers/maps/map-info.parser"; import { CustomError } from "shared/models/exceptions/custom-error.class"; import { tryit } from "shared/helpers/error.helpers"; -import { extractZip, processZip } from "main/helpers/zip.helpers"; +import { YauzlZip } from "main/models/yauzl-zip.class"; export class LocalMapsManagerService { private static instance: LocalMapsManagerService; @@ -324,35 +324,15 @@ export class LocalMapsManagerService { public importMaps(zipPaths: string[], version?: BSVersion): Observable> { return new Observable>(obs => { - let unsubscribed = false; let progress: Progression = { total: 0, current: 0 }; let nbImportedMaps = 0; + const abortController = new AbortController(); + let zip: YauzlZip; (async () => { - const terminate = () => unsubscribed; - let completeNewFolder: () => void; - const newFolderTap = tap((folder: string) => { - this.loadMapInfoFromPath(folder) - .then(map => { - progress.data = map; - }) - .catch(error => { - log.error("Could not load map info", error); - progress.data = null; - }) - .finally(() => { - ++nbImportedMaps; - ++progress.current; - obs.next(progress); - if (progress.current === progress.total) { - completeNewFolder(); - } - }); - }); - const mapsPath = await this.getMapsFolderPath(version); for(const zipPath of zipPaths) { - if(unsubscribed) { + if(abortController.signal?.aborted) { log.info("Maps import from zip has been cancelled"); return; } @@ -362,59 +342,68 @@ export class LocalMapsManagerService { if(!pathExistsSync(zipPath)) { continue; } - const { result: mapsFolders, error } = await tryit(() => processZip( - zipPath, {}, - (set, entry) => { - if (!entry.directory && /(^|\/)[Ii]nfo\.dat$/.test(entry.name)) { - set.add(path.dirname(entry.name)); - } - return set; - }, - new Set()) - ); + const mapsFolders: string[] = []; + zip = await YauzlZip.fromPath(zipPath); + for await (const entry of zip.entries()) { + if (/(^|\/)[Ii]nfo\.dat$/.test(entry.fileName)) { + mapsFolders.push(path.dirname(entry.fileName)); + } + } - if (error) { - log.error("Could not read zip entries", zipPath, error); + if (mapsFolders.length === 0) { + log.warn("No maps \"info.dat\" found in zip", zipPath); + progress.total = 1; + progress.current = 1; + obs.next(progress); continue; } - if (mapsFolders.size === 0) { - log.warn("No maps \"info.dat\" found in zip", zipPath); - } - - progress.total = mapsFolders.size; + progress.total = mapsFolders.length; obs.next(progress); - const newFolder = new Subject(); - const promise = lastValueFrom(newFolder.pipe(newFolderTap)); - completeNewFolder = () => newFolder.complete(); - - const isRoot = mapsFolders.size === 1 && mapsFolders.has("."); + const isRoot = mapsFolders.length === 1 && mapsFolders[0] === "."; const destination = isRoot ? path.join(mapsPath, path.basename(zipPath, ".zip")) : mapsPath; log.info("Extracting", `"${zipPath}"`, "into", `"${mapsPath}"`); - await extractZip(zipPath, destination, { - terminate, - condition: !isRoot && ((entry) => mapsFolders.has(path.dirname(entry.name))), - beforeFolderExtracted: (folder) => { - if (!mapsFolders.has(folder)) return; - log.info("*", `"${folder}"`, "before"); - }, - afterFolderExtracted: (folder) => { - if (!mapsFolders.has(folder)) return; - newFolder.next(path.resolve(destination, folder)); - } - }); + for (const folder of mapsFolders) { + log.info("*", folder); + const regex = new RegExp(`^${folder + .replaceAll(".", "\\.") + .replaceAll("+", "\\+") + .replaceAll("(", "\\(") + .replaceAll(")", "\\)") + .replaceAll("[", "\\[") + .replaceAll("]", "\\]") + }\/`); - if (unsubscribed) { + await zip.extract(destination, { + entriesNames: [regex], + abortToken: abortController + }); + + if (abortController.signal?.aborted) { + break; + } + + ++nbImportedMaps; + ++progress.current; + progress.data = await this.loadMapInfoFromPath(path.join(destination, folder)); + obs.next(progress); + + if (abortController.signal?.aborted) { + break; + } + } + + zip.close(); + + if (abortController.signal?.aborted) { log.info("Maps import from zip has been cancelled"); return; } - - await promise; } })() .then(() => { @@ -424,10 +413,15 @@ export class LocalMapsManagerService { return log.info("Successfully imported", nbImportedMaps, "maps from", zipPaths.length, "zips"); }) .catch(e => obs.error(e)) - .finally(() => obs.complete()); + .finally(() => { + if (zip) { + zip.close(); + } + obs.complete() + }); return () => { - unsubscribed = true; + abortController.abort(); }; }); } @@ -455,7 +449,9 @@ export class LocalMapsManagerService { } const zipPath = await this.downloadMapZip(zipUrl); - await extractZip(zipPath, mapPath); + const zip = await YauzlZip.fromPath(zipPath); + await zip.extract(mapPath); + zip.close(); await unlink(zipPath); const localMap = await this.loadMapInfoFromPath(mapPath); diff --git a/src/main/services/mods/bs-mods-manager.service.ts b/src/main/services/mods/bs-mods-manager.service.ts index a2535099..64f1682e 100644 --- a/src/main/services/mods/bs-mods-manager.service.ts +++ b/src/main/services/mods/bs-mods-manager.service.ts @@ -10,7 +10,6 @@ import { BS_EXECUTABLE } from "../../constants"; import log from "electron-log"; import { deleteFolder, ensureFolderExist, pathExist, Progression, unlinkPath } from "../../helpers/fs.helpers"; import { lastValueFrom, Observable } from "rxjs"; -import { extractZip, processZip } from "../../helpers/zip.helpers"; import recursiveReadDir from "recursive-readdir"; import { sToMs } from "../../../shared/helpers/time.helpers"; import { pathExistsSync, unlinkSync } from "fs-extra"; @@ -20,6 +19,7 @@ import { LinuxService } from "../linux.service"; import { tryit } from "shared/helpers/error.helpers"; import { UtilsService } from "../utils.service"; import crypto from "crypto"; +import { YauzlZip } from "main/models/yauzl-zip.class"; export class BsModsManagerService { private static instance: BsModsManagerService; @@ -205,16 +205,15 @@ export class BsModsManagerService { return false; } - const hashCount = await processZip(zipPath, { - getBuffer: true - }, (acc, entry) => { - if (entry.directory) return acc; - + const zip = await YauzlZip.fromPath(zipPath); + let hashCount = 0; + for await (const entry of zip.entries()) { + const buffer = await entry.read(); const md5Hash = crypto.createHash("md5") - .update(entry.buffer) + .update(buffer) .digest("hex"); - return acc + +download.hashMd5.some(md5 => md5.hash === md5Hash); - }, 0); + hashCount += +download.hashMd5.some(md5 => md5.hash === md5Hash); + } if (hashCount !== download.hashMd5.length) { return false; @@ -225,13 +224,14 @@ export class BsModsManagerService { const destDir = isBSIPA ? versionPath : path.join(versionPath, ModsInstallFolder.PENDING); log.info("Start extracting mod zip", mod.name, "to", destDir); - const extracted = await extractZip(zipPath, destDir) + const extracted = await zip.extract(destDir) .then(() => true) .catch(e => { log.error("Error while extracting mod zip", e); return false; }) .finally(() => { + zip.close(); if (pathExistsSync(zipPath)) { unlinkSync(zipPath); }