Skip to content

Commit d1c25da

Browse files
committed
Adds DatabaseDecompressionUtil Class
This class is responsible for decompressing database archives. It's initialized on server start, but the decompression mechanism only fires when the target database directory is empty. Adds roughly 1.5 seconds to initial server start (in my limited testing). The gulp build script has been updated to ignore the location database files. Needs to be tested.
1 parent 2d920f1 commit d1c25da

File tree

6 files changed

+155
-5
lines changed

6 files changed

+155
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:78828905f10b6c6617de192ceae582b4c49538d2a347172e3f65da20c2a98df9
3+
size 22259059

project/gulpfile.mjs

+14-3
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,20 @@ const updateBuildProperties = async () => {
122122
*/
123123
const copyAssets = () =>
124124
gulp
125-
.src(["assets/**/*.json", "assets/**/*.json5", "assets/**/*.png", "assets/**/*.jpg", "assets/**/*.ico"], {
126-
encoding: false,
127-
})
125+
.src(
126+
[
127+
"assets/**/*.json",
128+
"assets/**/*.json5",
129+
"assets/**/*.png",
130+
"assets/**/*.jpg",
131+
"assets/**/*.ico",
132+
"assets/**/*.7z",
133+
"!assets/database/locations/**/*.json", // Included in a 7z archive.
134+
],
135+
{
136+
encoding: false,
137+
},
138+
)
128139
.pipe(gulp.dest(dataDir));
129140

130141
/**

project/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
"typescript": "~5.5",
5656
"winston": "~3.13",
5757
"winston-daily-rotate-file": "~5.0",
58+
"7zip-bin": "^5.2.0",
59+
"node-7z": "^3.0.0",
5860
"ws": "~8.18"
5961
},
6062
"devDependencies": {
@@ -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,127 @@
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+
13+
constructor(@inject("PrimaryLogger") protected logger: ILogger) {
14+
this.compressedDir = path.resolve(__dirname, "../../assets/compressed/database");
15+
this.assetsDir = path.resolve(__dirname, "../../assets/database");
16+
}
17+
18+
/**
19+
* Initializes the database compression utility.
20+
*
21+
* This method will decompress all 7-zip archives within the compressed database directory.
22+
* The decompressed files are placed in their respective directories based on the name
23+
* and location of the compressed file.
24+
*/
25+
public async initialize(): Promise<void> {
26+
try {
27+
const compressedFiles = await this.getCompressedFiles();
28+
if (compressedFiles.length === 0) {
29+
this.logger.debug("No database archives found");
30+
return;
31+
}
32+
33+
for (const compressedFile of compressedFiles) {
34+
await this.processCompressedFile(compressedFile);
35+
}
36+
this.logger.info("Database archives processed");
37+
} catch (error) {
38+
this.logger.error(`Error handling database archives: ${error}`);
39+
}
40+
}
41+
42+
/**
43+
* Retrieves a list of all 7-zip archives within the compressed database directory.
44+
*/
45+
private async getCompressedFiles(): Promise<string[]> {
46+
try {
47+
const files = await fs.readdir(this.compressedDir);
48+
const compressedFiles = files.filter((file) => file.endsWith(".7z"));
49+
return compressedFiles;
50+
} catch (error) {
51+
this.logger.error(`Error reading database archive directory: ${error}`);
52+
return [];
53+
}
54+
}
55+
56+
/**
57+
* Processes a compressed file by checking if the target directory is empty,
58+
* and if so, decompressing the file into the target directory.
59+
*/
60+
private async processCompressedFile(compressedFileName: string): Promise<void> {
61+
this.logger.info("Processing database archives...");
62+
63+
const compressedFilePath = path.join(this.compressedDir, compressedFileName);
64+
const relativeTargetPath = compressedFileName.replace(".7z", "");
65+
const targetDir = path.join(this.assetsDir, relativeTargetPath);
66+
67+
try {
68+
this.logger.debug(`Processing: ${compressedFileName}`);
69+
70+
const isTargetDirEmpty = await this.isDirectoryEmpty(targetDir);
71+
if (!isTargetDirEmpty) {
72+
this.logger.debug(`Archive target directory not empty, skipping: ${targetDir}`);
73+
return;
74+
}
75+
76+
await this.decompressFile(compressedFilePath, targetDir);
77+
78+
this.logger.debug(`Successfully processed: ${compressedFileName}`);
79+
} catch (error) {
80+
this.logger.error(`Error processing ${compressedFileName}: ${error}`);
81+
}
82+
}
83+
84+
/**
85+
* Checks if a directory exists and is empty.
86+
*/
87+
private async isDirectoryEmpty(directoryPath: string): Promise<boolean> {
88+
try {
89+
const exists = await fs.pathExists(directoryPath);
90+
if (!exists) {
91+
return true; // Directory doesn't exist, consider it empty.
92+
}
93+
const files = await fs.readdir(directoryPath);
94+
return files.length === 0;
95+
} catch (error) {
96+
this.logger.error(`Error checking if directory is empty ${directoryPath}: ${error}`);
97+
throw error;
98+
}
99+
}
100+
101+
/**
102+
* Decompresses a 7-zip archive to the target directory.
103+
*/
104+
private decompressFile(archivePath: string, destinationPath: string): Promise<void> {
105+
return new Promise((resolve, reject) => {
106+
const myStream = Seven.extractFull(archivePath, destinationPath, {
107+
$bin: path7za,
108+
overwrite: "a",
109+
});
110+
111+
let hadError = false;
112+
113+
myStream.on("end", () => {
114+
if (!hadError) {
115+
this.logger.debug(`Decompressed ${archivePath} to ${destinationPath}`);
116+
resolve();
117+
}
118+
});
119+
120+
myStream.on("error", (err) => {
121+
hadError = true;
122+
this.logger.error(`Error decompressing ${archivePath}: ${err}`);
123+
reject(err);
124+
});
125+
});
126+
}
127+
}

0 commit comments

Comments
 (0)