[bugfix] some adjutments + some fixes + add zip tests

This commit is contained in:
MathieuG-P
2024-11-17 12:20:50 +01:00
parent 07d94962c4
commit 59b7b9aedb
16 changed files with 271 additions and 81 deletions
@@ -3,7 +3,7 @@ import path from "path";
import { Readable } from "stream";
import yauzl, { ZipFile, Options, Entry } from "yauzl"
export class YauzlZip {
export class BsmZipExtractor {
private static readonly YAUZL_OPEN_OPTIONS: Options = {
lazyEntries: true,
@@ -11,36 +11,36 @@ export class YauzlZip {
autoClose: false,
};
public static fromPath(path: string): Promise<YauzlZip> {
public static fromPath(path: string): Promise<BsmZipExtractor> {
return new Promise((resolve, reject) => {
yauzl.open(path, YauzlZip.YAUZL_OPEN_OPTIONS, (error: Error, zip: yauzl.ZipFile) => {
yauzl.open(path, BsmZipExtractor.YAUZL_OPEN_OPTIONS, (error: Error, zip: yauzl.ZipFile) => {
if (error) return reject(error);
resolve(new YauzlZip(zip));
resolve(new BsmZipExtractor(zip));
});
});
}
public static fromBuffer(buffer: Buffer): Promise<YauzlZip> {
public static fromBuffer(buffer: Buffer): Promise<BsmZipExtractor> {
return new Promise((resolve, reject) => {
yauzl.fromBuffer(buffer, YauzlZip.YAUZL_OPEN_OPTIONS, (error: Error, zip: yauzl.ZipFile) => {
yauzl.fromBuffer(buffer, BsmZipExtractor.YAUZL_OPEN_OPTIONS, (error: Error, zip: yauzl.ZipFile) => {
if (error) return reject(error);
resolve(new YauzlZip(zip));
resolve(new BsmZipExtractor(zip));
});
});
}
private readonly zip: ZipFile;
private readonly entriesMap = new Map<string, YauzlZipEntry>();
private readonly entriesMap = new Map<string, BsmZipExtractorEntry>();
private constructor(zip: ZipFile) {
this.zip = zip;
}
private async readEntry(): Promise<YauzlZipEntry> {
private async readEntry(): Promise<BsmZipExtractorEntry> {
return new Promise((resolve, reject) => {
const onEntry = (entry: Entry) => {
cleanup();
resolve(new YauzlZipEntry({ entry, zip: this.zip }));
resolve(new BsmZipExtractorEntry({ entry, zip: this.zip }));
};
const onEnd = () => {
@@ -68,13 +68,13 @@ export class YauzlZip {
// The first time this method is called, it will read all the entries and store them in a map
// The next times it will return the entries from the map to avoid reopening the zip file again
public async *entries(): AsyncGenerator<YauzlZipEntry> {
public async *entries(): AsyncGenerator<BsmZipExtractorEntry> {
for (const entry of Array.from(this.entriesMap.values())) {
yield entry;
}
let entry: YauzlZipEntry = await this.readEntry();
let entry: BsmZipExtractorEntry = await this.readEntry();
while (entry) {
this.entriesMap.set(entry.fileName, entry);
yield entry;
@@ -82,7 +82,7 @@ export class YauzlZip {
}
}
public async findEntry(func: (entry: YauzlZipEntry) => boolean): Promise<YauzlZipEntry|null> {
public async findEntry(func: (entry: BsmZipExtractorEntry) => boolean): Promise<BsmZipExtractorEntry|null> {
for await (const entry of this.entries()) {
if (func(entry)) {
return entry;
@@ -91,6 +91,20 @@ export class YauzlZip {
return null;
}
public async filterEntries(func: (entry: BsmZipExtractorEntry) => boolean): Promise<BsmZipExtractorEntry[]> {
const filtered: BsmZipExtractorEntry[] = [];
for await (const entry of this.entries()) {
if (func(entry)) {
filtered.push(entry);
}
}
return filtered;
}
public async getEntry(fileName: string): Promise<BsmZipExtractorEntry> {
return this.entriesMap.get(fileName) ?? this.findEntry(entry => entry.fileName === fileName);
}
/**
* Extracts all entries from the zip file to the destination folder
* @param destination
@@ -149,7 +163,7 @@ export class YauzlZip {
}
class YauzlZipEntry {
class BsmZipExtractorEntry {
private readonly entry: Entry;
private readonly zip: ZipFile;
+14 -9
View File
@@ -6,7 +6,8 @@ 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 { YauzlZip } from "./yauzl-zip.class";
import { BsmZipExtractor } from "./bsm-zip-extractor.class";
import { tryit } from "shared/helpers/error.helpers";
export class OculusDownloader {
@@ -34,19 +35,23 @@ export class OculusDownloader {
const buffer = await this.downloadManifestZip(downloadUrl)
.catch(err => CustomError.throw(err, "DOWNLOAD_MANIFEST_FAILED"));
const manifestName = "manifest.json";
const zip = await YauzlZip.fromBuffer(buffer);
const entry = await zip.findEntry((entry) => entry.fileName === manifestName);
const zip = await BsmZipExtractor.fromBuffer(buffer);
const entry = await zip.getEntry("manifest.json");
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"))
.finally(() => {
zip.close();
});
zip.close();
const { result, error } = tryit(() => JSON.parse(manifest.toString()) as OculusManifest);
if(error){
throw CustomError.throw(error, "PARSE_MANIFEST_FILE_FAILED")
}
return result;
}
private downloadManifestFile(file: OculusManifestFile, destination: string): Observable<Progression<OculusManifestFile>> {
@@ -29,7 +29,8 @@ 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 { YauzlZip } from "main/models/yauzl-zip.class";
import { BsmZipExtractor } from "main/models/bsm-zip-extractor.class";
import { escapeRegExp } from "../../../../shared/helpers/string.helpers";
export class LocalMapsManagerService {
private static instance: LocalMapsManagerService;
@@ -327,7 +328,7 @@ export class LocalMapsManagerService {
let progress: Progression<BsmLocalMap> = { total: 0, current: 0 };
let nbImportedMaps = 0;
const abortController = new AbortController();
let zip: YauzlZip;
let zip: BsmZipExtractor;
(async () => {
const mapsPath = await this.getMapsFolderPath(version);
@@ -342,14 +343,9 @@ export class LocalMapsManagerService {
if(!pathExistsSync(zipPath)) { continue; }
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));
}
}
zip = await BsmZipExtractor.fromPath(zipPath);
const mapsFolders = (await zip.filterEntries(entry => /(^|\/)[Ii]nfo\.dat$/.test(entry.fileName)))
.map(entry => path.dirname(entry.fileName));
if (mapsFolders.length === 0) {
log.warn("No maps \"info.dat\" found in zip", zipPath);
@@ -370,20 +366,19 @@ export class LocalMapsManagerService {
log.info("Extracting", `"${zipPath}"`, "into", `"${mapsPath}"`);
for (const folder of mapsFolders) {
log.info(">", folder);
const regex = new RegExp(`^${folder
.replaceAll(".", "\\.")
.replaceAll("+", "\\+")
.replaceAll("(", "\\(")
.replaceAll(")", "\\)")
.replaceAll("[", "\\[")
.replaceAll("]", "\\]")
}\\/`);
await zip.extract(destination, {
const regex = new RegExp(`^${escapeRegExp(folder)}\\/`);
const exported = await zip.extract(destination, {
entriesNames: [regex],
abortToken: abortController
});
if(exported.length === 0) {
log.warn("No files extracted from", folder);
continue;
}
if (abortController.signal?.aborted) {
break;
}
@@ -449,7 +444,7 @@ export class LocalMapsManagerService {
}
const zipPath = await this.downloadMapZip(zipUrl);
const zip = await YauzlZip.fromPath(zipPath);
const zip = await BsmZipExtractor.fromPath(zipPath);
await zip.extract(mapPath);
zip.close();
await unlink(zipPath);
@@ -8,18 +8,18 @@ import { RequestService } from "../request.service";
import { spawn } from "child_process";
import { BS_EXECUTABLE } from "../../constants";
import log from "electron-log";
import { deleteFolder, ensureFolderExist, pathExist, Progression, unlinkPath } from "../../helpers/fs.helpers";
import { deleteFolder, pathExist, Progression, unlinkPath } from "../../helpers/fs.helpers";
import { lastValueFrom, Observable } from "rxjs";
import recursiveReadDir from "recursive-readdir";
import { sToMs } from "../../../shared/helpers/time.helpers";
import { pathExistsSync, unlinkSync } from "fs-extra";
import { pathExistsSync } from "fs-extra";
import { CustomError } from "shared/models/exceptions/custom-error.class";
import { popElement } from "shared/helpers/array.helpers";
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";
import { BsmZipExtractor } from "main/models/bsm-zip-extractor.class";
export class BsModsManagerService {
private static instance: BsModsManagerService;
@@ -113,22 +113,23 @@ export class BsModsManagerService {
return this.beatModsApi.getModByHash(injectorMd5);
}
private async downloadZip(zipUrl: string): Promise<string> {
private async downloadZip(zipUrl: string): Promise<BsmZipExtractor> {
zipUrl = path.join(this.beatModsApi.BEAT_MODS_URL, zipUrl);
log.info("Download mod zip", zipUrl);
const fileName = `${path.basename(zipUrl, ".zip")}-${crypto.randomUUID()}.zip`;
const tempPath = this.utilsService.getTempPath();
const destination = path.join(tempPath, fileName);
const buffer = await lastValueFrom(this.requestService.downloadBuffer(zipUrl))
.then(progress => progress.data)
.catch(e => {
log.error("ZIP", "Error while downloading zip", e);
return undefined;
});
try {
await ensureFolderExist(tempPath);
return (await lastValueFrom(this.requestService.downloadFile(zipUrl, destination))).data;
} catch (error) {
log.error("Could not download zip file at", zipUrl);
if (!buffer) {
return null;
}
return BsmZipExtractor.fromBuffer(buffer);
}
private async executeBSIPA(version: BSVersion, args: string[]): Promise<boolean> {
@@ -199,13 +200,13 @@ export class BsModsManagerService {
}
log.info("Start download mod zip", mod.name, download.url);
const zipPath = await this.downloadZip(download.url);
log.info("Mod zip download end", mod.name, download.url, !!zipPath);
if (!zipPath) {
const zip = await this.downloadZip(download.url);
log.info("Mod zip download end", mod.name, download.url);
if (!zip) {
return false;
}
const zip = await YauzlZip.fromPath(zipPath);
let hashCount = 0;
for await (const entry of zip.entries()) {
const buffer = await entry.read();
@@ -232,9 +233,6 @@ export class BsModsManagerService {
})
.finally(() => {
zip.close();
if (pathExistsSync(zipPath)) {
unlinkSync(zipPath);
}
});
log.info("Mod zip extraction end", mod.name, "to", destDir, "success:", extracted);