From fdae8709ab4a47b341ce5a53e5f65ae5d54b2342 Mon Sep 17 00:00:00 2001 From: Refringe Date: Thu, 28 Nov 2024 10:03:44 -0500 Subject: [PATCH] DatabaseDecompressionUtil Class Reintroduces the the `DatabaseDecompressionUtil` class. This baby will automatically decompress database archives if their target directory is empty or does not exist. It will only run in a non-compiled environment, so only developers (and 31337 linux h4x0rs) will be able to utilize it. --- project/package.json | 4 +- project/src/Program.ts | 5 + project/src/di/Container.ts | 4 + .../src/utils/DatabaseDecompressionUtil.ts | 143 ++++++++++++++++++ 4 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 project/src/utils/DatabaseDecompressionUtil.ts diff --git a/project/package.json b/project/package.json index 9e749eece..019392686 100644 --- a/project/package.json +++ b/project/package.json @@ -37,6 +37,7 @@ "database:decompress": "node scripts/databaseDecompress.js" }, "dependencies": { + "7zip-bin": "^5.2.0", "atomically": "~1.7", "buffer-crc32": "~1.0", "date-fns": "~3.6", @@ -46,6 +47,7 @@ "json5": "~2.2", "jsonc": "~2.0", "mongoid-js": "~1.3", + "node-7z": "^3.0.0", "proper-lockfile": "~4.1", "reflect-metadata": "~0.2", "semver": "~7.6", @@ -71,7 +73,6 @@ "@vitest/ui": "~2", "@yao-pkg/pkg": "5.12", "@yao-pkg/pkg-fetch": "3.5.9", - "7zip-bin": "^5.2.0", "cross-env": "~7.0", "fs-extra": "~11.2", "gulp": "~5.0", @@ -81,7 +82,6 @@ "gulp-rename": "~2.0", "madge": "~7", "minimist": "~1.2", - "node-7z": "^3.0.0", "resedit": "~2.0", "ts-node-dev": "~2.0", "tsconfig-paths": "~4.2", diff --git a/project/src/Program.ts b/project/src/Program.ts index 167cdc94a..a90294a7c 100644 --- a/project/src/Program.ts +++ b/project/src/Program.ts @@ -2,6 +2,7 @@ import { ErrorHandler } from "@spt/ErrorHandler"; import { Container } from "@spt/di/Container"; import type { PreSptModLoader } from "@spt/loaders/PreSptModLoader"; import { App } from "@spt/utils/App"; +import { DatabaseDecompressionUtil } from "@spt/utils/DatabaseDecompressionUtil"; import { Watermark } from "@spt/utils/Watermark"; import { container } from "tsyringe"; @@ -21,6 +22,10 @@ export class Program { const watermark = childContainer.resolve("Watermark"); watermark.initialize(); + const databaseDecompressionUtil = + childContainer.resolve("DatabaseDecompressionUtil"); + await databaseDecompressionUtil.initialize(); + const preSptModLoader = childContainer.resolve("PreSptModLoader"); Container.registerListTypes(childContainer); await preSptModLoader.load(childContainer); diff --git a/project/src/di/Container.ts b/project/src/di/Container.ts index 4433426c4..d66367ab2 100644 --- a/project/src/di/Container.ts +++ b/project/src/di/Container.ts @@ -253,6 +253,7 @@ import { StaticRouterModService } from "@spt/services/mod/staticRouter/StaticRou import { App } from "@spt/utils/App"; import { AsyncQueue } from "@spt/utils/AsyncQueue"; import { CompareUtil } from "@spt/utils/CompareUtil"; +import { DatabaseDecompressionUtil } from "@spt/utils/DatabaseDecompressionUtil"; import { DatabaseImporter } from "@spt/utils/DatabaseImporter"; import { EncodingUtil } from "@spt/utils/EncodingUtil"; import { HashUtil } from "@spt/utils/HashUtil"; @@ -419,6 +420,9 @@ export class Container { private static registerUtils(depContainer: DependencyContainer): void { // Utils depContainer.register("App", App, { lifecycle: Lifecycle.Singleton }); + depContainer.register("DatabaseDecompressionUtil", DatabaseDecompressionUtil, { + lifecycle: Lifecycle.Singleton, + }); depContainer.register("DatabaseImporter", DatabaseImporter, { lifecycle: Lifecycle.Singleton, }); diff --git a/project/src/utils/DatabaseDecompressionUtil.ts b/project/src/utils/DatabaseDecompressionUtil.ts new file mode 100644 index 000000000..17ca893df --- /dev/null +++ b/project/src/utils/DatabaseDecompressionUtil.ts @@ -0,0 +1,143 @@ +import * as path from "node:path"; +import { path7za } from "7zip-bin"; +import { ILogger } from "@spt/models/spt/utils/ILogger"; +import * as fs from "fs-extra"; +import * as Seven from "node-7z"; +import { inject, injectable } from "tsyringe"; + +@injectable() +export class DatabaseDecompressionUtil { + private compressedDir: string; + private assetsDir: string; + private compiled: boolean; + + constructor(@inject("PrimaryLogger") protected logger: ILogger) { + this.compressedDir = path.normalize("./assets/compressed/database"); + this.assetsDir = path.normalize("./assets/database"); + this.compiled = this.isCompiled(); + } + + /** + * Checks if the application is running in a compiled environment. A simple check is done to see if the relative + * assets directory exists. If it does not, the application is assumed to be running in a compiled environment. All + * relative asset paths are different within a compiled environment, so this simple check is sufficient. + */ + private isCompiled(): boolean { + const assetsDir = path.normalize("./assets"); + return !fs.existsSync(assetsDir); + } + + /** + * Initializes the database compression utility. + * + * This method will decompress all 7-zip archives within the compressed database directory. The decompressed files + * are placed in their respective directories based on the name and location of the compressed file. + */ + public async initialize(): Promise { + if (this.compiled) { + this.logger.debug("Skipping database decompression in compiled environment"); + return; + } + + try { + const compressedFiles = await this.getCompressedFiles(); + if (compressedFiles.length === 0) { + this.logger.debug("No database archives found"); + return; + } + + for (const compressedFile of compressedFiles) { + await this.processCompressedFile(compressedFile); + } + this.logger.info("Database archives processed"); + } catch (error) { + this.logger.error(`Error handling database archives: ${error}`); + } + } + + /** + * Retrieves a list of all 7-zip archives within the compressed database directory. + */ + private async getCompressedFiles(): Promise { + try { + const files = await fs.readdir(this.compressedDir); + const compressedFiles = files.filter((file) => file.endsWith(".7z")); + return compressedFiles; + } catch (error) { + this.logger.error(`Error reading database archive directory: ${error}`); + return []; + } + } + + /** + * Processes a compressed file by checking if the target directory is empty, and if so, decompressing the file into + * the target directory. + */ + private async processCompressedFile(compressedFileName: string): Promise { + this.logger.info("Processing database archives..."); + + const compressedFilePath = path.join(this.compressedDir, compressedFileName); + const relativeTargetPath = compressedFileName.replace(".7z", ""); + const targetDir = path.join(this.assetsDir, relativeTargetPath); + + try { + this.logger.debug(`Processing: ${compressedFileName}`); + + const isTargetDirEmpty = await this.isDirectoryEmpty(targetDir); + if (!isTargetDirEmpty) { + this.logger.debug(`Archive target directory not empty, skipping: ${targetDir}`); + return; + } + + await this.decompressFile(compressedFilePath, targetDir); + + this.logger.debug(`Successfully processed: ${compressedFileName}`); + } catch (error) { + this.logger.error(`Error processing ${compressedFileName}: ${error}`); + } + } + + /** + * Checks if a directory exists and is empty. + */ + private async isDirectoryEmpty(directoryPath: string): Promise { + try { + const exists = await fs.pathExists(directoryPath); + if (!exists) { + return true; // Directory doesn't exist, consider it empty. + } + const files = await fs.readdir(directoryPath); + return files.length === 0; + } catch (error) { + this.logger.error(`Error checking if directory is empty ${directoryPath}: ${error}`); + throw error; + } + } + + /** + * Decompresses a 7-zip archive to the target directory. + */ + private decompressFile(archivePath: string, destinationPath: string): Promise { + return new Promise((resolve, reject) => { + const myStream = Seven.extractFull(archivePath, destinationPath, { + $bin: path7za, + overwrite: "a", + }); + + let hadError = false; + + myStream.on("end", () => { + if (!hadError) { + this.logger.debug(`Decompressed ${archivePath} to ${destinationPath}`); + resolve(); + } + }); + + myStream.on("error", (err) => { + hadError = true; + this.logger.error(`Error decompressing ${archivePath}: ${err}`); + reject(err); + }); + }); + } +}