Skip to content

Commit fdae870

Browse files
committed
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.
1 parent 5e3be9b commit fdae870

File tree

4 files changed

+154
-2
lines changed

4 files changed

+154
-2
lines changed

project/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"database:decompress": "node scripts/databaseDecompress.js"
3838
},
3939
"dependencies": {
40+
"7zip-bin": "^5.2.0",
4041
"atomically": "~1.7",
4142
"buffer-crc32": "~1.0",
4243
"date-fns": "~3.6",
@@ -46,6 +47,7 @@
4647
"json5": "~2.2",
4748
"jsonc": "~2.0",
4849
"mongoid-js": "~1.3",
50+
"node-7z": "^3.0.0",
4951
"proper-lockfile": "~4.1",
5052
"reflect-metadata": "~0.2",
5153
"semver": "~7.6",
@@ -71,7 +73,6 @@
7173
"@vitest/ui": "~2",
7274
"@yao-pkg/pkg": "5.12",
7375
"@yao-pkg/pkg-fetch": "3.5.9",
74-
"7zip-bin": "^5.2.0",
7576
"cross-env": "~7.0",
7677
"fs-extra": "~11.2",
7778
"gulp": "~5.0",
@@ -81,7 +82,6 @@
8182
"gulp-rename": "~2.0",
8283
"madge": "~7",
8384
"minimist": "~1.2",
84-
"node-7z": "^3.0.0",
8585
"resedit": "~2.0",
8686
"ts-node-dev": "~2.0",
8787
"tsconfig-paths": "~4.2",

project/src/Program.ts

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ErrorHandler } from "@spt/ErrorHandler";
22
import { Container } from "@spt/di/Container";
33
import type { PreSptModLoader } from "@spt/loaders/PreSptModLoader";
44
import { App } from "@spt/utils/App";
5+
import { DatabaseDecompressionUtil } from "@spt/utils/DatabaseDecompressionUtil";
56
import { Watermark } from "@spt/utils/Watermark";
67
import { container } from "tsyringe";
78

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

25+
const databaseDecompressionUtil =
26+
childContainer.resolve<DatabaseDecompressionUtil>("DatabaseDecompressionUtil");
27+
await databaseDecompressionUtil.initialize();
28+
2429
const preSptModLoader = childContainer.resolve<PreSptModLoader>("PreSptModLoader");
2530
Container.registerListTypes(childContainer);
2631
await preSptModLoader.load(childContainer);

project/src/di/Container.ts

+4
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ import { StaticRouterModService } from "@spt/services/mod/staticRouter/StaticRou
253253
import { App } from "@spt/utils/App";
254254
import { AsyncQueue } from "@spt/utils/AsyncQueue";
255255
import { CompareUtil } from "@spt/utils/CompareUtil";
256+
import { DatabaseDecompressionUtil } from "@spt/utils/DatabaseDecompressionUtil";
256257
import { DatabaseImporter } from "@spt/utils/DatabaseImporter";
257258
import { EncodingUtil } from "@spt/utils/EncodingUtil";
258259
import { HashUtil } from "@spt/utils/HashUtil";
@@ -419,6 +420,9 @@ export class Container {
419420
private static registerUtils(depContainer: DependencyContainer): void {
420421
// Utils
421422
depContainer.register<App>("App", App, { lifecycle: Lifecycle.Singleton });
423+
depContainer.register<DatabaseDecompressionUtil>("DatabaseDecompressionUtil", DatabaseDecompressionUtil, {
424+
lifecycle: Lifecycle.Singleton,
425+
});
422426
depContainer.register<DatabaseImporter>("DatabaseImporter", DatabaseImporter, {
423427
lifecycle: Lifecycle.Singleton,
424428
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import * as path from "node:path";
2+
import { path7za } from "7zip-bin";
3+
import { ILogger } from "@spt/models/spt/utils/ILogger";
4+
import * as fs from "fs-extra";
5+
import * as Seven from "node-7z";
6+
import { inject, injectable } from "tsyringe";
7+
8+
@injectable()
9+
export class DatabaseDecompressionUtil {
10+
private compressedDir: string;
11+
private assetsDir: string;
12+
private compiled: boolean;
13+
14+
constructor(@inject("PrimaryLogger") protected logger: ILogger) {
15+
this.compressedDir = path.normalize("./assets/compressed/database");
16+
this.assetsDir = path.normalize("./assets/database");
17+
this.compiled = this.isCompiled();
18+
}
19+
20+
/**
21+
* Checks if the application is running in a compiled environment. A simple check is done to see if the relative
22+
* assets directory exists. If it does not, the application is assumed to be running in a compiled environment. All
23+
* relative asset paths are different within a compiled environment, so this simple check is sufficient.
24+
*/
25+
private isCompiled(): boolean {
26+
const assetsDir = path.normalize("./assets");
27+
return !fs.existsSync(assetsDir);
28+
}
29+
30+
/**
31+
* Initializes the database compression utility.
32+
*
33+
* This method will decompress all 7-zip archives within the compressed database directory. The decompressed files
34+
* are placed in their respective directories based on the name and location of the compressed file.
35+
*/
36+
public async initialize(): Promise<void> {
37+
if (this.compiled) {
38+
this.logger.debug("Skipping database decompression in compiled environment");
39+
return;
40+
}
41+
42+
try {
43+
const compressedFiles = await this.getCompressedFiles();
44+
if (compressedFiles.length === 0) {
45+
this.logger.debug("No database archives found");
46+
return;
47+
}
48+
49+
for (const compressedFile of compressedFiles) {
50+
await this.processCompressedFile(compressedFile);
51+
}
52+
this.logger.info("Database archives processed");
53+
} catch (error) {
54+
this.logger.error(`Error handling database archives: ${error}`);
55+
}
56+
}
57+
58+
/**
59+
* Retrieves a list of all 7-zip archives within the compressed database directory.
60+
*/
61+
private async getCompressedFiles(): Promise<string[]> {
62+
try {
63+
const files = await fs.readdir(this.compressedDir);
64+
const compressedFiles = files.filter((file) => file.endsWith(".7z"));
65+
return compressedFiles;
66+
} catch (error) {
67+
this.logger.error(`Error reading database archive directory: ${error}`);
68+
return [];
69+
}
70+
}
71+
72+
/**
73+
* Processes a compressed file by checking if the target directory is empty, and if so, decompressing the file into
74+
* the target directory.
75+
*/
76+
private async processCompressedFile(compressedFileName: string): Promise<void> {
77+
this.logger.info("Processing database archives...");
78+
79+
const compressedFilePath = path.join(this.compressedDir, compressedFileName);
80+
const relativeTargetPath = compressedFileName.replace(".7z", "");
81+
const targetDir = path.join(this.assetsDir, relativeTargetPath);
82+
83+
try {
84+
this.logger.debug(`Processing: ${compressedFileName}`);
85+
86+
const isTargetDirEmpty = await this.isDirectoryEmpty(targetDir);
87+
if (!isTargetDirEmpty) {
88+
this.logger.debug(`Archive target directory not empty, skipping: ${targetDir}`);
89+
return;
90+
}
91+
92+
await this.decompressFile(compressedFilePath, targetDir);
93+
94+
this.logger.debug(`Successfully processed: ${compressedFileName}`);
95+
} catch (error) {
96+
this.logger.error(`Error processing ${compressedFileName}: ${error}`);
97+
}
98+
}
99+
100+
/**
101+
* Checks if a directory exists and is empty.
102+
*/
103+
private async isDirectoryEmpty(directoryPath: string): Promise<boolean> {
104+
try {
105+
const exists = await fs.pathExists(directoryPath);
106+
if (!exists) {
107+
return true; // Directory doesn't exist, consider it empty.
108+
}
109+
const files = await fs.readdir(directoryPath);
110+
return files.length === 0;
111+
} catch (error) {
112+
this.logger.error(`Error checking if directory is empty ${directoryPath}: ${error}`);
113+
throw error;
114+
}
115+
}
116+
117+
/**
118+
* Decompresses a 7-zip archive to the target directory.
119+
*/
120+
private decompressFile(archivePath: string, destinationPath: string): Promise<void> {
121+
return new Promise((resolve, reject) => {
122+
const myStream = Seven.extractFull(archivePath, destinationPath, {
123+
$bin: path7za,
124+
overwrite: "a",
125+
});
126+
127+
let hadError = false;
128+
129+
myStream.on("end", () => {
130+
if (!hadError) {
131+
this.logger.debug(`Decompressed ${archivePath} to ${destinationPath}`);
132+
resolve();
133+
}
134+
});
135+
136+
myStream.on("error", (err) => {
137+
hadError = true;
138+
this.logger.error(`Error decompressing ${archivePath}: ${err}`);
139+
reject(err);
140+
});
141+
});
142+
}
143+
}

0 commit comments

Comments
 (0)