diff --git a/package-lock.json b/package-lock.json index cfc5e2ed..d8574f6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,11 +32,9 @@ "got": "^14.4.4", "history": "^5.3.0", "is-elevated": "^4.0.0", - "jszip": "^3.10.1", "md5-file": "^5.0.0", "node-abi": "^3.65.0", "node-fetch": "^3.3.2", - "node-stream-zip": "^1.15.0", "pako": "^2.1.0", "protobufjs": "^7.4.0", "qrcode.react": "^4.0.1", @@ -13897,6 +13895,7 @@ }, "node_modules/immediate": { "version": "3.0.6", + "dev": true, "license": "MIT" }, "node_modules/immutable": { @@ -15813,6 +15812,7 @@ }, "node_modules/jszip": { "version": "3.10.1", + "dev": true, "license": "(MIT OR GPL-3.0-or-later)", "dependencies": { "lie": "~3.3.0", @@ -15823,10 +15823,12 @@ }, "node_modules/jszip/node_modules/pako": { "version": "1.0.11", + "dev": true, "license": "(MIT AND Zlib)" }, "node_modules/jszip/node_modules/readable-stream": { "version": "2.3.7", + "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -15944,6 +15946,7 @@ }, "node_modules/lie": { "version": "3.3.0", + "dev": true, "license": "MIT", "dependencies": { "immediate": "~3.0.5" @@ -17230,17 +17233,6 @@ "dev": true, "license": "MIT" }, - "node_modules/node-stream-zip": { - "version": "1.15.0", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/antelle" - } - }, "node_modules/nopt": { "version": "6.0.0", "dev": true, @@ -20047,6 +20039,7 @@ }, "node_modules/setimmediate": { "version": "1.0.5", + "dev": true, "license": "MIT" }, "node_modules/setprototypeof": { diff --git a/package.json b/package.json index 6e87a389..f9dfce73 100644 --- a/package.json +++ b/package.json @@ -225,11 +225,9 @@ "got": "^14.4.4", "history": "^5.3.0", "is-elevated": "^4.0.0", - "jszip": "^3.10.1", "md5-file": "^5.0.0", "node-abi": "^3.65.0", "node-fetch": "^3.3.2", - "node-stream-zip": "^1.15.0", "pako": "^2.1.0", "protobufjs": "^7.4.0", "qrcode.react": "^4.0.1", diff --git a/src/main/helpers/zip.helpers.ts b/src/main/helpers/zip.helpers.ts index 463f8e53..a5444fd4 100644 --- a/src/main/helpers/zip.helpers.ts +++ b/src/main/helpers/zip.helpers.ts @@ -23,6 +23,8 @@ export interface ZipExtractOptions { }; export interface ZipProcessOptions { + // Stops the extraction immediately when set to true + terminate?: (entry: ZipEntry) => boolean; getBuffer?: boolean; }; @@ -96,7 +98,8 @@ function handleExtractZip( zipEntry.buffer = undefined; if (options?.terminate?.(zipEntry)) { - return readStream.destroy(); + readStream.destroy(); + return zip.close(); } if (options?.condition) { @@ -129,23 +132,28 @@ function handleExtractZip( // @params loop - this should not throw an error export function processZip( - zipPath: string, + data: string | Buffer, options: Readonly, loop: (accumulator: T, entry: ZipEntry) => T, initValue: T ): Promise { return new Promise((resolve, reject) => { - yauzl.open(zipPath, { - lazyEntries: true, - decodeStrings: true, - }, - (openError, zip) => { + const callback = (openError: Error, zip: yauzl.ZipFile) => { if (openError) return reject(openError); handleProcessZip( zip, options, loop, initValue, resolve, reject ); - }); + } + + if (typeof data === "string") { + yauzl.open(data, { + lazyEntries: true, + decodeStrings: true, + }, callback); + } else { + yauzl.fromBuffer(data, callback); + } }); } @@ -177,14 +185,21 @@ async function handleProcessZip( 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; + } + const chunks: Buffer[] = []; readStream.on("data", data => { chunks.push(data); }); readStream.on("end", () => { - zipEntry.name = entry.fileName; - zipEntry.directory = false; zipEntry.buffer = Buffer.concat(chunks); accumulator = loop(accumulator, zipEntry); zip.readEntry(); diff --git a/src/main/models/oculus-downloader.class.ts b/src/main/models/oculus-downloader.class.ts index 47cc9067..3078ef0a 100644 --- a/src/main/models/oculus-downloader.class.ts +++ b/src/main/models/oculus-downloader.class.ts @@ -1,4 +1,3 @@ -import JSZip from "jszip"; import fetch from "node-fetch"; import { CustomError } from "../../shared/models/exceptions/custom-error.class"; import { mkdirs, createWriteStream, pathExists, WriteStream } from "fs-extra"; @@ -7,6 +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 { processZip } from "main/helpers/zip.helpers"; export class OculusDownloader { @@ -23,26 +23,40 @@ export class OculusDownloader { return `https://securecdn.oculus.com/binaries/segment/?access_token=${token}&binary_id=${binaryId}&segment_sha256=${segmentSha256}`; } - private async downloadManifestZip(manifestUrl: string): Promise { + private async downloadManifestZip(manifestUrl: string): Promise { const response = await fetch(manifestUrl); const arrBuffer = await response.arrayBuffer(); - return JSZip.loadAsync(arrBuffer); + return Buffer.from(arrBuffer); } private async getManifest(): Promise { const downloadUrl = this.getDownloadManifestUrl(this.options.accessToken, this.options.binaryId); - const manifestZip = await this.downloadManifestZip(downloadUrl).catch(err => CustomError.throw(err, "DOWNLOAD_MANIFEST_FAILED")); - const manifestFile = manifestZip.file("manifest.json"); + const buffer = await this.downloadManifestZip(downloadUrl) + .catch(err => CustomError.throw(err, "DOWNLOAD_MANIFEST_FAILED")); - if(!manifestFile){ + let found = false; + let manifestString: string | null = null; + + // NOTE: Might be better to do get file in zip.helpers + await processZip(buffer, { + terminate: () => found, + getBuffer: true, + }, (_, entry): void => { + if (entry.name !== "manifest.json") return; + found = true; + manifestString = entry.buffer.toString(); + }, undefined); + + if(!manifestString) { throw new CustomError("Manifest file not found", "MANIFEST_FILE_NOT_FOUND"); } - return manifestFile.async("text").then(JSON.parse).catch(err => CustomError.throw(err, "PARSE_MANIFEST_FILE_FAILED")); + return JSON.parse(manifestString) + .catch((err: Error) => CustomError.throw(err, "PARSE_MANIFEST_FILE_FAILED")); } private downloadManifestFile(file: OculusManifestFile, destination: string): Observable> { - + const downloadSegment = async (segment: OculusManifestFileSegment): Promise => { const segmentUrl = this.getDownloadSegmentUrl(this.options.accessToken, this.options.binaryId, segment[1]); const response = await fetch(segmentUrl); @@ -66,11 +80,11 @@ export class OculusDownloader { const arrBuffer = await downloadSegment(segment); const inflated = inflate(arrBuffer); - await writeStream.write(inflated); + writeStream.write(inflated); progress.current += inflated.byteLength; progress.diff = inflated.byteLength; - + sub.next(progress); } @@ -86,7 +100,7 @@ export class OculusDownloader { }); } - private isFileIntegrityValid(file: OculusFileWithName, folder: string): Promise { + private async isFileIntegrityValid(file: OculusFileWithName, folder: string): Promise { const [fileName, fileData] = file; const destination = path.join(folder, fileName); @@ -138,7 +152,7 @@ export class OculusDownloader { if(this.isDownloading){ throw new CustomError("Already downloading", "ALREADY_DOWNLOADING"); } - + this.options = options; this.isDownloading = true; @@ -182,7 +196,7 @@ export class OculusDownloader { }))); const integrity = await lastValueFrom(this.verifyIntegrity(manifest, options.destination)).catch(err => CustomError.throw(err, "VERIFY_INTEGRITY_FAILED")); - + if(integrity.data.length > 0){ throw new CustomError("Some files failed to download", "SOME_FILES_FAILED_TO_DOWNLOAD", integrity.data); } @@ -242,4 +256,4 @@ interface Logger { info: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void; -} \ No newline at end of file +}