Skip to content

Commit

Permalink
File System Classes
Browse files Browse the repository at this point in the history
This adds the `FileSystem` and `FileSystemSync` classes to replace the VFS class. These classes handle file system operations using `fs-extra` for most tasks, except where the `atomically` package can be used to improve reads and writes. The goal is to ensure that file operations are as safe as possible while still providing a comfortable API. File operation atomicity is focused on single files, as there's no trivial, strict way to ensure atomicity for directory operations.

## Changes
- Adds `FileSystem` class for asynchronous file operations
- Adds `FileSystemSync` class for synchronous file operations
- Updates `atomically` to `2.0.3`
- Removes `VFS` class
- Removes `AsyncQueue` class

# TODO
- Test with mods (transpiles)
- Test compiled build
  • Loading branch information
refringe committed Jan 8, 2025
1 parent 3273c32 commit dc5a11c
Show file tree
Hide file tree
Showing 30 changed files with 878 additions and 593 deletions.
6 changes: 2 additions & 4 deletions project/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,17 @@
"gen:customisationstorage": "tsx ./src/tools/HideoutCustomisation/HideoutCustomisationProgram.ts"
},
"dependencies": {
"atomically": "~1.7",
"atomically": "2.0.3",
"buffer-crc32": "~1.0",
"date-fns": "~3.6",
"date-fns-tz": "~3.1",
"fs-extra": "^11.2.0",
"fs-extra": "11.2.0",
"i18n": "~0.15",
"json-fixer": "~1.6",
"json5": "~2.2",
"jsonc": "~2.0",
"logform": "~2.6",
"mongoid-js": "~1.3",
"proper-lockfile": "~4.1",
"reflect-metadata": "~0.2",
"semver": "~7.6",
"source-map-support": "~0.5",
Expand All @@ -65,7 +64,6 @@
"@types/fs-extra": "11.0.4",
"@types/i18n": "~0.13",
"@types/node": "22.10.2",
"@types/proper-lockfile": "~4.1",
"@types/semver": "~7.5",
"@types/ws": "~8.5",
"@vitest/coverage-istanbul": "^2.1.8",
Expand Down
7 changes: 5 additions & 2 deletions project/src/ErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import readline from "node:readline";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { AsyncQueue } from "@spt/utils/AsyncQueue";
import { WinstonMainLogger } from "@spt/utils/logging/WinstonMainLogger";
import { FileSystem } from "./utils/FileSystem";
import { FileSystemSync } from "./utils/FileSystemSync";

export class ErrorHandler {
private logger: ILogger;
private readLine: readline.Interface;

constructor() {
this.logger = new WinstonMainLogger(new AsyncQueue());
const fileSystem = new FileSystem();
const fileSystemSync = new FileSystemSync();
this.logger = new WinstonMainLogger(fileSystem, fileSystemSync);
this.readLine = readline.createInterface({ input: process.stdin, output: process.stdout });
}

Expand Down
9 changes: 4 additions & 5 deletions project/src/di/Container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ import { ModTypeCheck } from "@spt/loaders/ModTypeCheck";
import { PostDBModLoader } from "@spt/loaders/PostDBModLoader";
import { PostSptModLoader } from "@spt/loaders/PostSptModLoader";
import { PreSptModLoader } from "@spt/loaders/PreSptModLoader";
import { IAsyncQueue } from "@spt/models/spt/utils/IAsyncQueue";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { EventOutputHolder } from "@spt/routers/EventOutputHolder";
import { HttpRouter } from "@spt/routers/HttpRouter";
Expand Down Expand Up @@ -256,10 +255,11 @@ import { OnLoadModService } from "@spt/services/mod/onLoad/OnLoadModService";
import { OnUpdateModService } from "@spt/services/mod/onUpdate/OnUpdateModService";
import { StaticRouterModService } from "@spt/services/mod/staticRouter/StaticRouterModService";
import { App } from "@spt/utils/App";
import { AsyncQueue } from "@spt/utils/AsyncQueue";
import { CompareUtil } from "@spt/utils/CompareUtil";
import { DatabaseImporter } from "@spt/utils/DatabaseImporter";
import { EncodingUtil } from "@spt/utils/EncodingUtil";
import { FileSystem } from "@spt/utils/FileSystem";
import { FileSystemSync } from "@spt/utils/FileSystemSync";
import { HashUtil } from "@spt/utils/HashUtil";
import { HttpFileUtil } from "@spt/utils/HttpFileUtil";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
Expand All @@ -269,7 +269,6 @@ import { MathUtil } from "@spt/utils/MathUtil";
import { ObjectId } from "@spt/utils/ObjectId";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { VFS } from "@spt/utils/VFS";
import { Watermark, WatermarkLocale } from "@spt/utils/Watermark";
import type { ICloner } from "@spt/utils/cloners/ICloner";
import { JsonCloner } from "@spt/utils/cloners/JsonCloner";
Expand Down Expand Up @@ -443,10 +442,10 @@ export class Container {
depContainer.register<ObjectId>("ObjectId", ObjectId);
depContainer.register<RandomUtil>("RandomUtil", RandomUtil, { lifecycle: Lifecycle.Singleton });
depContainer.register<TimeUtil>("TimeUtil", TimeUtil, { lifecycle: Lifecycle.Singleton });
depContainer.register<VFS>("VFS", VFS, { lifecycle: Lifecycle.Singleton });
depContainer.register<FileSystem>("FileSystem", FileSystem, { lifecycle: Lifecycle.Singleton });
depContainer.register<FileSystemSync>("FileSystemSync", FileSystemSync, { lifecycle: Lifecycle.Singleton });
depContainer.register<WatermarkLocale>("WatermarkLocale", WatermarkLocale, { lifecycle: Lifecycle.Singleton });
depContainer.register<Watermark>("Watermark", Watermark, { lifecycle: Lifecycle.Singleton });
depContainer.register<IAsyncQueue>("AsyncQueue", AsyncQueue, { lifecycle: Lifecycle.Singleton });
depContainer.register<HttpFileUtil>("HttpFileUtil", HttpFileUtil, { lifecycle: Lifecycle.Singleton });
depContainer.register<ModLoadOrder>("ModLoadOrder", ModLoadOrder, { lifecycle: Lifecycle.Singleton });
depContainer.register<ModTypeCheck>("ModTypeCheck", ModTypeCheck, { lifecycle: Lifecycle.Singleton });
Expand Down
10 changes: 4 additions & 6 deletions project/src/loaders/BundleLoader.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import path from "node:path";
import { HttpServerHelper } from "@spt/helpers/HttpServerHelper";
import { BundleHashCacheService } from "@spt/services/cache/BundleHashCacheService";
import { FileSystemSync } from "@spt/utils/FileSystemSync";
import { JsonUtil } from "@spt/utils/JsonUtil";
import { VFS } from "@spt/utils/VFS";
import type { ICloner } from "@spt/utils/cloners/ICloner";
import { inject, injectable } from "tsyringe";

Expand All @@ -26,7 +25,7 @@ export class BundleLoader {

constructor(
@inject("HttpServerHelper") protected httpServerHelper: HttpServerHelper,
@inject("VFS") protected vfs: VFS,
@inject("FileSystemSync") protected fileSystemSync: FileSystemSync,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("BundleHashCacheService") protected bundleHashCacheService: BundleHashCacheService,
@inject("PrimaryCloner") protected cloner: ICloner,
Expand All @@ -50,9 +49,8 @@ export class BundleLoader {
}

public addBundles(modpath: string): void {
const bundleManifestArr = this.jsonUtil.deserialize<IBundleManifest>(
this.vfs.readFile(`${modpath}bundles.json`),
).manifest;
const bundles = this.fileSystemSync.readJson(`${modpath}bundles.json`) as IBundleManifest;
const bundleManifestArr = bundles?.manifest;

for (const bundleManifest of bundleManifestArr) {
const relativeModPath = modpath.slice(0, -1).replace(/\\/g, "/");
Expand Down
51 changes: 26 additions & 25 deletions project/src/loaders/PreSptModLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import type { ILogger } from "@spt/models/spt/utils/ILogger";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { LocalisationService } from "@spt/services/LocalisationService";
import { ModCompilerService } from "@spt/services/ModCompilerService";
import { FileSystemSync } from "@spt/utils/FileSystemSync";
import { JsonUtil } from "@spt/utils/JsonUtil";
import { VFS } from "@spt/utils/VFS";
import { maxSatisfying, satisfies, valid, validRange } from "semver";
import { DependencyContainer, inject, injectable } from "tsyringe";

Expand All @@ -34,7 +34,7 @@ export class PreSptModLoader implements IModLoader {

constructor(
@inject("PrimaryLogger") protected logger: ILogger,
@inject("VFS") protected vfs: VFS,
@inject("FileSystemSync") protected fileSystemSync: FileSystemSync,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("ModCompilerService") protected modCompilerService: ModCompilerService,
@inject("LocalisationService") protected localisationService: LocalisationService,
Expand All @@ -45,7 +45,7 @@ export class PreSptModLoader implements IModLoader {
this.sptConfig = this.configServer.getConfig<ICoreConfig>(ConfigTypes.CORE);

const packageJsonPath: string = path.join(__dirname, "../../package.json");
this.serverDependencies = JSON.parse(this.vfs.readFile(packageJsonPath)).dependencies;
this.serverDependencies = this.fileSystemSync.readJson(packageJsonPath)?.dependencies;
this.skippedMods = new Set();
}

Expand Down Expand Up @@ -103,28 +103,28 @@ export class PreSptModLoader implements IModLoader {
}

protected async importModsAsync(): Promise<void> {
if (!this.vfs.exists(this.basepath)) {
if (!this.fileSystemSync.exists(this.basepath)) {
// no mods folder found
this.logger.info(this.localisationService.getText("modloader-user_mod_folder_missing"));
this.vfs.createDir(this.basepath);
this.fileSystemSync.ensureDir(this.basepath);
return;
}

/**
* array of mod folder names
*/
const mods: string[] = this.vfs.getDirs(this.basepath);
const mods: string[] = this.fileSystemSync.getDirectories(this.basepath);

this.logger.info(this.localisationService.getText("modloader-loading_mods", mods.length));

// Mod order
if (!this.vfs.exists(this.modOrderPath)) {
if (!this.fileSystemSync.exists(this.modOrderPath)) {
this.logger.info(this.localisationService.getText("modloader-mod_order_missing"));

// Write file with empty order array to disk
this.vfs.writeFile(this.modOrderPath, this.jsonUtil.serializeAdvanced({ order: [] }, undefined, 4));
this.fileSystemSync.writeJson(this.modOrderPath, { order: [] });
} else {
const modOrder = this.vfs.readFile(this.modOrderPath, { encoding: "utf8" });
const modOrder = this.fileSystemSync.read(this.modOrderPath);
try {
const modOrderArray = this.jsonUtil.deserialize<any>(modOrder, this.modOrderPath).order;
for (const [index, mod] of modOrderArray.entries()) {
Expand Down Expand Up @@ -154,7 +154,7 @@ export class PreSptModLoader implements IModLoader {
if (
modToValidate.dependencies &&
Object.keys(modToValidate.dependencies).length > 0 &&
!this.vfs.exists(`${this.basepath}${modFolderName}/node_modules`)
!this.fileSystemSync.exists(`${this.basepath}${modFolderName}/node_modules`)
) {
this.autoInstallDependencies(`${this.basepath}${modFolderName}`, modToValidate);
}
Expand Down Expand Up @@ -274,7 +274,7 @@ export class PreSptModLoader implements IModLoader {
const loadedMods = new Map<string, IPackageJsonData>();

for (const mod of mods) {
loadedMods.set(mod, this.jsonUtil.deserialize(this.vfs.readFile(`${this.getModPath(mod)}/package.json`)));
loadedMods.set(mod, this.fileSystemSync.readJson(`${this.getModPath(mod)}/package.json`));
}

return loadedMods;
Expand Down Expand Up @@ -380,8 +380,8 @@ export class PreSptModLoader implements IModLoader {
public sortModsLoadOrder(): string[] {
// if loadorder.json exists: load it, otherwise generate load order
const loadOrderPath = `${this.basepath}loadorder.json`;
if (this.vfs.exists(loadOrderPath)) {
return this.jsonUtil.deserialize(this.vfs.readFile(loadOrderPath), loadOrderPath);
if (this.fileSystemSync.exists(loadOrderPath)) {
return this.fileSystemSync.readJson(loadOrderPath);
}

return this.modLoadOrder.getLoadOrder();
Expand All @@ -394,7 +394,7 @@ export class PreSptModLoader implements IModLoader {
protected async addModAsync(mod: string, pkg: IPackageJsonData): Promise<void> {
const modPath = this.getModPath(mod);

const typeScriptFiles = this.vfs.getFilesOfType(`${modPath}src`, ".ts");
const typeScriptFiles = this.fileSystemSync.getFiles(`${modPath}src`, true, ["ts"]);

if (typeScriptFiles.length > 0) {
if (ProgramStatics.COMPILED) {
Expand Down Expand Up @@ -468,9 +468,10 @@ export class PreSptModLoader implements IModLoader {
return;
}

// Temporarily rename package.json because otherwise npm, pnpm and any other package manager will forcefully download all packages in dependencies without any way of disabling this behavior
this.vfs.rename(`${modPath}/package.json`, `${modPath}/package.json.bak`);
this.vfs.writeFile(`${modPath}/package.json`, "{}");
// Temporarily rename package.json because otherwise npm, pnpm and any other package manager will forcefully
// download all packages in dependencies without any way of disabling this behavior
this.fileSystemSync.rename(`${modPath}/package.json`, `${modPath}/package.json.bak`);
this.fileSystemSync.writeJson(`${modPath}/package.json`, {});

this.logger.info(
this.localisationService.getText("modloader-installing_external_dependencies", {
Expand All @@ -494,8 +495,8 @@ export class PreSptModLoader implements IModLoader {
execSync(command, { cwd: modPath });

// Delete the new blank package.json then rename the backup back to the original name
this.vfs.removeFile(`${modPath}/package.json`);
this.vfs.rename(`${modPath}/package.json.bak`, `${modPath}/package.json`);
this.fileSystemSync.remove(`${modPath}/package.json`);
this.fileSystemSync.rename(`${modPath}/package.json.bak`, `${modPath}/package.json`);
}

protected areModDependenciesFulfilled(pkg: IPackageJsonData, loadedMods: Map<string, IPackageJsonData>): boolean {
Expand Down Expand Up @@ -568,8 +569,8 @@ export class PreSptModLoader implements IModLoader {
const modIsCalledUser = modName.toLowerCase() === "user";
const modIsCalledSrc = modName.toLowerCase() === "src";
const modIsCalledDb = modName.toLowerCase() === "db";
const hasBepinExFolderStructure = this.vfs.exists(`${modPath}/plugins`);
const containsDll = this.vfs.getFiles(`${modPath}`).find((x) => x.includes(".dll"));
const hasBepinExFolderStructure = this.fileSystemSync.exists(`${modPath}/plugins`);
const containsDll = this.fileSystemSync.getFiles(`${modPath}`, true, ["dll"]).length > 0;

if (modIsCalledSrc || modIsCalledDb || modIsCalledUser) {
this.logger.error(this.localisationService.getText("modloader-not_correct_mod_folder", modName));
Expand All @@ -583,13 +584,13 @@ export class PreSptModLoader implements IModLoader {

// Check if config exists
const modPackagePath = `${modPath}/package.json`;
if (!this.vfs.exists(modPackagePath)) {
if (!this.fileSystemSync.exists(modPackagePath)) {
this.logger.error(this.localisationService.getText("modloader-missing_package_json", modName));
return false;
}

// Validate mod
const config = this.jsonUtil.deserialize<IPackageJsonData>(this.vfs.readFile(modPackagePath), modPackagePath);
const config = this.fileSystemSync.readJson(modPackagePath) as IPackageJsonData;
const checks = ["name", "author", "version", "license"];
let issue = false;

Expand Down Expand Up @@ -617,10 +618,10 @@ export class PreSptModLoader implements IModLoader {
issue = true;
}

if (!this.vfs.exists(`${modPath}/${config.main}`)) {
if (!this.fileSystemSync.exists(`${modPath}/${config.main}`)) {
// If TS file exists with same name, dont perform check as we'll generate JS from TS file
const tsFileName = config.main.replace(".js", ".ts");
const tsFileExists = this.vfs.exists(`${modPath}/${tsFileName}`);
const tsFileExists = this.fileSystemSync.exists(`${modPath}/${tsFileName}`);

if (!tsFileExists) {
this.logger.error(
Expand Down
5 changes: 0 additions & 5 deletions project/src/models/spt/utils/IAsyncQueue.ts

This file was deleted.

4 changes: 0 additions & 4 deletions project/src/models/spt/utils/ICommand.ts

This file was deleted.

5 changes: 2 additions & 3 deletions project/src/routers/ImageRouter.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { IncomingMessage, ServerResponse } from "node:http";
import { ImageRouteService } from "@spt/services/mod/image/ImageRouteService";
import { FileSystemSync } from "@spt/utils/FileSystemSync";
import { HttpFileUtil } from "@spt/utils/HttpFileUtil";
import { VFS } from "@spt/utils/VFS";
import { inject, injectable } from "tsyringe";

@injectable()
export class ImageRouter {
constructor(
@inject("VFS") protected vfs: VFS,
@inject("ImageRouteService") protected imageRouteService: ImageRouteService,
@inject("HttpFileUtil") protected httpFileUtil: HttpFileUtil,
) {}
Expand All @@ -18,7 +17,7 @@ export class ImageRouter {

public async sendImage(sessionID: string, req: IncomingMessage, resp: ServerResponse, body: any): Promise<void> {
// remove file extension
const url = this.vfs.stripExtension(req.url);
const url = req.url ? FileSystemSync.stripExtension(req.url) : "";

// send image
if (this.imageRouteService.existsByKey(url)) {
Expand Down
32 changes: 12 additions & 20 deletions project/src/servers/ConfigServer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ProgramStatics } from "@spt/ProgramStatics";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import type { ILogger } from "@spt/models/spt/utils/ILogger";
import { FileSystemSync } from "@spt/utils/FileSystemSync";
import { JsonUtil } from "@spt/utils/JsonUtil";
import { VFS } from "@spt/utils/VFS";
import { inject, injectable } from "tsyringe";

@injectable()
Expand All @@ -12,7 +12,7 @@ export class ConfigServer {

constructor(
@inject("PrimaryLogger") protected logger: ILogger,
@inject("VFS") protected vfs: VFS,
@inject("FileSystemSync") protected fileSystemSync: FileSystemSync,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
) {
this.initialize();
Expand All @@ -35,29 +35,21 @@ export class ConfigServer {

// Get all filepaths
const filepath = ProgramStatics.COMPILED ? "SPT_Data/Server/configs/" : "./assets/configs/";
const files = this.vfs.getFiles(filepath);
const files = this.fileSystemSync.getFiles(filepath, true, this.acceptableFileExtensions);

// Add file content to result
for (const file of files) {
if (this.acceptableFileExtensions.includes(this.vfs.getFileExtension(file.toLowerCase()))) {
const fileName = this.vfs.stripExtension(file);
const filePathAndName = `${filepath}${file}`;
const deserialsiedJson = this.jsonUtil.deserializeJsonC<any>(
this.vfs.readFile(filePathAndName),
filePathAndName,
);

if (!deserialsiedJson) {
this.logger.error(
`Config file: ${filePathAndName} is corrupt. Use a site like: https://jsonlint.com to find the issue.`,
);
throw new Error(
`Server will not run until the: ${filePathAndName} config error mentioned above is fixed`,
);
}
const fileName = FileSystemSync.getFileName(file);
const deserialsiedJson = this.jsonUtil.deserializeJsonC<any>(this.fileSystemSync.read(file), fileName);

this.configs[`spt-${fileName}`] = deserialsiedJson;
if (!deserialsiedJson) {
this.logger.error(
`Config file: ${fileName} is corrupt. Use a site like: https://jsonlint.com to find the issue.`,
);
throw new Error(`Server will not run until the: ${fileName} config error mentioned above is fixed`);
}

this.configs[`spt-${fileName}`] = deserialsiedJson;
}

this.logger.info(`Commit hash: ${ProgramStatics.COMMIT || "DEBUG"}`);
Expand Down
Loading

0 comments on commit dc5a11c

Please sign in to comment.