Skip to content

Commit

Permalink
DatabaseDecompressionUtil Class
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
refringe committed Nov 28, 2024
1 parent 5e3be9b commit fdae870
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 2 deletions.
4 changes: 2 additions & 2 deletions project/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions project/src/Program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -21,6 +22,10 @@ export class Program {
const watermark = childContainer.resolve<Watermark>("Watermark");
watermark.initialize();

const databaseDecompressionUtil =
childContainer.resolve<DatabaseDecompressionUtil>("DatabaseDecompressionUtil");
await databaseDecompressionUtil.initialize();

const preSptModLoader = childContainer.resolve<PreSptModLoader>("PreSptModLoader");
Container.registerListTypes(childContainer);
await preSptModLoader.load(childContainer);
Expand Down
4 changes: 4 additions & 0 deletions project/src/di/Container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -419,6 +420,9 @@ export class Container {
private static registerUtils(depContainer: DependencyContainer): void {
// Utils
depContainer.register<App>("App", App, { lifecycle: Lifecycle.Singleton });
depContainer.register<DatabaseDecompressionUtil>("DatabaseDecompressionUtil", DatabaseDecompressionUtil, {
lifecycle: Lifecycle.Singleton,
});
depContainer.register<DatabaseImporter>("DatabaseImporter", DatabaseImporter, {
lifecycle: Lifecycle.Singleton,
});
Expand Down
143 changes: 143 additions & 0 deletions project/src/utils/DatabaseDecompressionUtil.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<string[]> {
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<void> {
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<boolean> {
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<void> {
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);
});
});
}
}

0 comments on commit fdae870

Please sign in to comment.