From 5982422e04974a3846aefc9bab5f8903667e4a24 Mon Sep 17 00:00:00 2001 From: Dennis Postma Date: Wed, 1 Jan 2025 21:34:23 +0100 Subject: [PATCH] Storage class is now OOP --- src/application/console/commandRegistry.ts | 6 +- src/application/storage.ts | 85 +++++++++++++------ src/commands/init.ts | 11 ++- src/commands/tiles.ts | 10 +-- .../gameMaster/assetManager/object/remove.ts | 6 +- .../gameMaster/assetManager/object/upload.ts | 6 +- .../gameMaster/assetManager/sprite/create.ts | 6 +- .../gameMaster/assetManager/sprite/delete.ts | 6 +- .../gameMaster/assetManager/sprite/update.ts | 6 +- .../gameMaster/assetManager/tile/delete.ts | 6 +- .../gameMaster/assetManager/tile/upload.ts | 6 +- src/http/controllers/assets.ts | 4 +- src/http/controllers/avatar.ts | 11 +-- src/managers/queueManager.ts | 6 +- src/managers/socketManager.ts | 6 +- 15 files changed, 109 insertions(+), 72 deletions(-) diff --git a/src/application/console/commandRegistry.ts b/src/application/console/commandRegistry.ts index 0925734..2faeee8 100644 --- a/src/application/console/commandRegistry.ts +++ b/src/application/console/commandRegistry.ts @@ -3,7 +3,7 @@ import * as path from 'path' import { pathToFileURL } from 'url' import Logger, { LoggerType } from '#application/logger' -import { getAppPath } from '#application/storage' +import Storage from '#application/storage' import { Command } from '#application/types' export class CommandRegistry { @@ -15,7 +15,7 @@ export class CommandRegistry { } public async loadCommands(): Promise { - const directory = getAppPath('commands') + const directory = Storage.getAppPath('commands') this.logger.info(`Loading commands from: ${directory}`) try { @@ -32,7 +32,7 @@ export class CommandRegistry { private async loadCommandFile(file: fs.Dirent): Promise { try { - const filePath = getAppPath('commands', file.name) + const filePath = Storage.getAppPath('commands', file.name) const commandName = path.basename(file.name, path.extname(file.name)) const module = await import(pathToFileURL(filePath).href) diff --git a/src/application/storage.ts b/src/application/storage.ts index 2b5cadb..053a761 100644 --- a/src/application/storage.ts +++ b/src/application/storage.ts @@ -1,34 +1,71 @@ import fs from 'fs' import path from 'path' -import config from './config' +import config from '#application/config' -export function getRootPath(folder: string, ...additionalSegments: string[]) { - return path.join(process.cwd(), folder, ...additionalSegments) -} +class Storage { + private readonly baseDir: string + private readonly rootDir: string -export function getAppPath(folder: string, ...additionalSegments: string[]) { - const baseDir = config.ENV === 'development' ? 'src' : 'dist' - return path.join(process.cwd(), baseDir, folder, ...additionalSegments) -} + constructor() { + this.rootDir = process.cwd() + this.baseDir = config.ENV === 'development' ? 'src' : 'dist' + } -export function getPublicPath(folder: string, ...additionalSegments: string[]) { - return path.join(process.cwd(), 'public', folder, ...additionalSegments) -} + /** + * Gets path relative to project root + */ + public getRootPath(folder: string, ...additionalSegments: string[]): string { + return path.join(this.rootDir, folder, ...additionalSegments) + } -export function doesPathExist(path: string) { - try { - fs.accessSync(path, fs.constants.F_OK) - return true - } catch (e) { - return false + /** + * Gets path relative to app directory (src/dist) + */ + public getAppPath(folder: string, ...additionalSegments: string[]): string { + return path.join(this.rootDir, this.baseDir, folder, ...additionalSegments) + } + + /** + * Gets path relative to public directory + */ + public getPublicPath(folder: string, ...additionalSegments: string[]): string { + return path.join(this.rootDir, 'public', folder, ...additionalSegments) + } + + /** + * Checks if a path exists + * @throws Error if path is empty or invalid + */ + public doesPathExist(pathToCheck: string): boolean { + if (!pathToCheck) { + throw new Error('Path cannot be empty') + } + + try { + fs.accessSync(pathToCheck, fs.constants.F_OK) + return true + } catch (e) { + return false + } + } + + /** + * Creates a directory and any necessary parent directories + * @throws Error if directory creation fails + */ + public createDir(dirPath: string): void { + if (!dirPath) { + throw new Error('Directory path cannot be empty') + } + + try { + fs.mkdirSync(dirPath, { recursive: true }) + } catch (error) { + const typedError = error as Error + throw new Error(`Failed to create directory: ${typedError.message}`) + } } } -export function createDir(path: string) { - try { - fs.mkdirSync(path, { recursive: true }) - } catch (e) { - console.error(e) - } -} +export default new Storage() diff --git a/src/commands/init.ts b/src/commands/init.ts index 4a08418..088d56d 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,11 +1,10 @@ import fs from 'fs' import sharp from 'sharp' -import { Server } from 'socket.io' import { BaseCommand } from '#application/base/baseCommand' import { CharacterGender, CharacterRace } from '#application/enums' -import { getPublicPath } from '#application/storage' +import Storage from '#application/storage' import { UUID } from '#application/types' import { Character } from '#entities/character' import { CharacterHair } from '#entities/characterHair' @@ -44,7 +43,7 @@ export default class InitCommand extends BaseCommand { } private async importTiles(): Promise { - for (const tile of fs.readdirSync(getPublicPath('tiles'))) { + for (const tile of fs.readdirSync(Storage.getPublicPath('tiles'))) { const newTile = new Tile() newTile.setId(tile.split('.')[0] as UUID).setName('New tile') @@ -53,18 +52,18 @@ export default class InitCommand extends BaseCommand { } private async importObjects(): Promise { - for (const object of fs.readdirSync(getPublicPath('objects'))) { + for (const object of fs.readdirSync(Storage.getPublicPath('objects'))) { const newMapObject = new MapObject() newMapObject .setId(object.split('.')[0] as UUID) .setName('New object') .setFrameWidth( - (await sharp(getPublicPath('objects', object)) + (await sharp(Storage.getPublicPath('objects', object)) .metadata() .then((metadata) => metadata.height)) ?? 0 ) .setFrameHeight( - (await sharp(getPublicPath('objects', object)) + (await sharp(Storage.getPublicPath('objects', object)) .metadata() .then((metadata) => metadata.width)) ?? 0 ) diff --git a/src/commands/tiles.ts b/src/commands/tiles.ts index 08a3667..531a9c8 100644 --- a/src/commands/tiles.ts +++ b/src/commands/tiles.ts @@ -3,12 +3,12 @@ import fs from 'fs' import sharp from 'sharp' import { BaseCommand } from '#application/base/baseCommand' -import { getPublicPath } from '#application/storage' +import Storage from '#application/storage' export default class TilesCommand extends BaseCommand { public async execute(): Promise { // Get all tiles - const tilesDir = getPublicPath('tiles') + const tilesDir = Storage.getPublicPath('tiles') const tiles = fs.readdirSync(tilesDir).filter((file) => file.endsWith('.png')) // Create output directory if it doesn't exist @@ -18,14 +18,14 @@ export default class TilesCommand extends BaseCommand { for (const tile of tiles) { // Check if tile is already 66x34 - const metadata = await sharp(getPublicPath('tiles', tile)).metadata() + const metadata = await sharp(Storage.getPublicPath('tiles', tile)).metadata() if (metadata.width === 66 && metadata.height === 34) { this.logger.info(`Tile ${tile} already processed`) continue } - const inputPath = getPublicPath('tiles', tile) - const tempPath = getPublicPath('tiles', `temp_${tile}`) + const inputPath = Storage.getPublicPath('tiles', tile) + const tempPath = Storage.getPublicPath('tiles', `temp_${tile}`) try { await sharp(inputPath) diff --git a/src/events/gameMaster/assetManager/object/remove.ts b/src/events/gameMaster/assetManager/object/remove.ts index 44d9dd9..6f63eef 100644 --- a/src/events/gameMaster/assetManager/object/remove.ts +++ b/src/events/gameMaster/assetManager/object/remove.ts @@ -4,7 +4,7 @@ import { Server } from 'socket.io' import { gameLogger, gameMasterLogger } from '#application/logger' import prisma from '#application/prisma' -import { getPublicPath } from '#application/storage' +import Storage from '#application/storage' import { TSocket } from '#application/types' import characterRepository from '#repositories/characterRepository' @@ -38,10 +38,10 @@ export default class ObjectRemoveEvent { }) // get root path - const public_folder = getPublicPath('objects') + const public_folder = Storage.getPublicPath('objects') // remove the tile from the disk - const finalFilePath = getPublicPath('objects', data.object + '.png') + const finalFilePath = Storage.getPublicPath('objects', data.object + '.png') fs.unlink(finalFilePath, (err) => { if (err) { gameMasterLogger.error(`Error deleting object ${data.object}: ${err.message}`) diff --git a/src/events/gameMaster/assetManager/object/upload.ts b/src/events/gameMaster/assetManager/object/upload.ts index e5eb6e3..840ee66 100644 --- a/src/events/gameMaster/assetManager/object/upload.ts +++ b/src/events/gameMaster/assetManager/object/upload.ts @@ -6,7 +6,7 @@ import { Server } from 'socket.io' import { gameMasterLogger } from '#application/logger' import prisma from '#application/prisma' -import { getPublicPath } from '#application/storage' +import Storage from '#application/storage' import { TSocket } from '#application/types' import characterRepository from '#repositories/characterRepository' @@ -32,7 +32,7 @@ export default class ObjectUploadEvent { if (character.role !== 'gm') { return callback(false) } - const public_folder = getPublicPath('objects') + const public_folder = Storage.getPublicPath('objects') // Ensure the folder exists await fs.mkdir(public_folder, { recursive: true }) @@ -56,7 +56,7 @@ export default class ObjectUploadEvent { const uuid = object.id const filename = `${uuid}.png` - const finalFilePath = getPublicPath('objects', filename) + const finalFilePath = Storage.getPublicPath('objects', filename) await writeFile(finalFilePath, objectData) gameMasterLogger.info('gm:object:upload', `Object ${key} uploaded with id ${uuid}`) diff --git a/src/events/gameMaster/assetManager/sprite/create.ts b/src/events/gameMaster/assetManager/sprite/create.ts index f5fd034..fc61769 100644 --- a/src/events/gameMaster/assetManager/sprite/create.ts +++ b/src/events/gameMaster/assetManager/sprite/create.ts @@ -3,7 +3,7 @@ import fs from 'fs/promises' import { Server } from 'socket.io' import prisma from '#application/prisma' -import { getPublicPath } from '#application/storage' +import Storage from '#application/storage' import { TSocket } from '#application/types' import characterRepository from '#repositories/characterRepository' @@ -26,7 +26,7 @@ export default class SpriteCreateEvent { return callback(false) } - const public_folder = getPublicPath('sprites') + const public_folder = Storage.getPublicPath('sprites') // Ensure the folder exists await fs.mkdir(public_folder, { recursive: true }) @@ -39,7 +39,7 @@ export default class SpriteCreateEvent { const uuid = sprite.id // Create folder with uuid - const sprite_folder = getPublicPath('sprites', uuid) + const sprite_folder = Storage.getPublicPath('sprites', uuid) await fs.mkdir(sprite_folder, { recursive: true }) callback(true) diff --git a/src/events/gameMaster/assetManager/sprite/delete.ts b/src/events/gameMaster/assetManager/sprite/delete.ts index 96ee456..e299547 100644 --- a/src/events/gameMaster/assetManager/sprite/delete.ts +++ b/src/events/gameMaster/assetManager/sprite/delete.ts @@ -4,7 +4,7 @@ import { Server } from 'socket.io' import { gameMasterLogger } from '#application/logger' import prisma from '#application/prisma' -import { getPublicPath } from '#application/storage' +import Storage from '#application/storage' import { TSocket } from '#application/types' import CharacterRepository from '#repositories/characterRepository' @@ -19,7 +19,7 @@ export default class GMSpriteDeleteEvent { private readonly io: Server, private readonly socket: TSocket ) { - this.public_folder = getPublicPath('sprites') + this.public_folder = Storage.getPublicPath('sprites') } public listen(): void { @@ -45,7 +45,7 @@ export default class GMSpriteDeleteEvent { } private async deleteSpriteFolder(spriteId: string): Promise { - const finalFilePath = getPublicPath('sprites', spriteId) + const finalFilePath = Storage.getPublicPath('sprites', spriteId) if (fs.existsSync(finalFilePath)) { await fs.promises.rmdir(finalFilePath, { recursive: true }) diff --git a/src/events/gameMaster/assetManager/sprite/update.ts b/src/events/gameMaster/assetManager/sprite/update.ts index 9019e3e..afcefa7 100644 --- a/src/events/gameMaster/assetManager/sprite/update.ts +++ b/src/events/gameMaster/assetManager/sprite/update.ts @@ -7,7 +7,7 @@ import type { Prisma, SpriteAction } from '@prisma/client' import { gameMasterLogger } from '#application/logger' import prisma from '#application/prisma' -import { getPublicPath } from '#application/storage' +import Storage from '#application/storage' import { TSocket } from '#application/types' import CharacterRepository from '#repositories/characterRepository' @@ -314,13 +314,13 @@ export default class SpriteUpdateEvent { } private async saveSpritesToDisk(id: string, actions: ProcessedSpriteAction[]): Promise { - const publicFolder = getPublicPath('sprites', id) + const publicFolder = Storage.getPublicPath('sprites', id) await mkdir(publicFolder, { recursive: true }) await Promise.all( actions.map(async (action) => { const spritesheet = await this.createSpritesheet(action.buffersWithDimensions) - await writeFile(getPublicPath('sprites', id, `${action.action}.png`), spritesheet) + await writeFile(Storage.getPublicPath('sprites', id, `${action.action}.png`), spritesheet) }) ) } diff --git a/src/events/gameMaster/assetManager/tile/delete.ts b/src/events/gameMaster/assetManager/tile/delete.ts index 9780432..3a97703 100644 --- a/src/events/gameMaster/assetManager/tile/delete.ts +++ b/src/events/gameMaster/assetManager/tile/delete.ts @@ -4,7 +4,7 @@ import { Server } from 'socket.io' import { gameMasterLogger } from '#application/logger' import prisma from '#application/prisma' -import { getPublicPath } from '#application/storage' +import Storage from '#application/storage' import { TSocket } from '#application/types' import characterRepository from '#repositories/characterRepository' @@ -19,7 +19,7 @@ export default class GMTileDeleteEvent { private readonly io: Server, private readonly socket: TSocket ) { - this.public_folder = getPublicPath('tiles') + this.public_folder = Storage.getPublicPath('tiles') } public listen(): void { @@ -56,7 +56,7 @@ export default class GMTileDeleteEvent { } private async deleteTileFile(tileId: string): Promise { - const finalFilePath = getPublicPath('tiles', `${tileId}.png`) + const finalFilePath = Storage.getPublicPath('tiles', `${tileId}.png`) try { await fs.unlink(finalFilePath) } catch (error: any) { diff --git a/src/events/gameMaster/assetManager/tile/upload.ts b/src/events/gameMaster/assetManager/tile/upload.ts index e05d4d3..dc47099 100644 --- a/src/events/gameMaster/assetManager/tile/upload.ts +++ b/src/events/gameMaster/assetManager/tile/upload.ts @@ -5,7 +5,7 @@ import { Server } from 'socket.io' import { gameMasterLogger } from '#application/logger' import prisma from '#application/prisma' -import { getPublicPath } from '#application/storage' +import Storage from '#application/storage' import { TSocket } from '#application/types' import characterRepository from '#repositories/characterRepository' @@ -32,7 +32,7 @@ export default class TileUploadEvent { return } - const public_folder = getPublicPath('tiles') + const public_folder = Storage.getPublicPath('tiles') // Ensure the folder exists await fs.mkdir(public_folder, { recursive: true }) @@ -45,7 +45,7 @@ export default class TileUploadEvent { }) const uuid = tile.id const filename = `${uuid}.png` - const finalFilePath = getPublicPath('tiles', filename) + const finalFilePath = Storage.getPublicPath('tiles', filename) await writeFile(finalFilePath, tileData) }) diff --git a/src/http/controllers/assets.ts b/src/http/controllers/assets.ts index 8407d49..758cb69 100644 --- a/src/http/controllers/assets.ts +++ b/src/http/controllers/assets.ts @@ -4,7 +4,7 @@ import { Request, Response } from 'express' import { BaseController } from '#application/base/baseController' import Database from '#application/database' -import { getPublicPath } from '#application/storage' +import Storage from '#application/storage' import { AssetData, UUID } from '#application/types' import SpriteRepository from '#repositories/spriteRepository' import TileRepository from '#repositories/tileRepository' @@ -98,7 +98,7 @@ export class AssetsController extends BaseController { public async downloadAsset(req: Request, res: Response) { const { type, spriteId, file } = req.params - const assetPath = type === 'sprites' && spriteId ? getPublicPath(type, spriteId, file) : getPublicPath(type, file) + const assetPath = type === 'sprites' && spriteId ? Storage.getPublicPath(type, spriteId, file) : Storage.getPublicPath(type, file) if (!fs.existsSync(assetPath)) { this.logger.error(`File not found: ${assetPath}`) diff --git a/src/http/controllers/avatar.ts b/src/http/controllers/avatar.ts index 2fc4b0d..9a503e7 100644 --- a/src/http/controllers/avatar.ts +++ b/src/http/controllers/avatar.ts @@ -4,14 +4,15 @@ import { Request, Response } from 'express' import sharp from 'sharp' import { BaseController } from '#application/base/baseController' -import { getPublicPath } from '#application/storage' +import Storage from '#application/storage' +import { UUID } from '#application/types' import CharacterHairRepository from '#repositories/characterHairRepository' import CharacterRepository from '#repositories/characterRepository' import CharacterTypeRepository from '#repositories/characterTypeRepository' interface AvatarOptions { - characterTypeId: number - characterHairId?: number + characterTypeId: UUID + characterHairId?: UUID } export class AvatarController extends BaseController { @@ -57,7 +58,7 @@ export class AvatarController extends BaseController { return this.sendError(res, 'Character type not found', 404) } - const bodySpritePath = getPublicPath('sprites', characterType.sprite.id, 'idle_right_down.png') + const bodySpritePath = Storage.getPublicPath('sprites', characterType.sprite.id, 'idle_right_down.png') if (!fs.existsSync(bodySpritePath)) { return this.sendError(res, 'Body sprite file not found', 404) } @@ -71,7 +72,7 @@ export class AvatarController extends BaseController { if (options.characterHairId) { const characterHair = await CharacterHairRepository.getById(options.characterHairId) if (characterHair?.sprite?.id) { - const hairSpritePath = getPublicPath('sprites', characterHair.sprite.id, 'front.png') + const hairSpritePath = Storage.getPublicPath('sprites', characterHair.sprite.id, 'front.png') if (fs.existsSync(hairSpritePath)) { avatar = avatar.composite([{ input: hairSpritePath, gravity: 'north' }]) } diff --git a/src/managers/queueManager.ts b/src/managers/queueManager.ts index d57ea8d..d76a33e 100644 --- a/src/managers/queueManager.ts +++ b/src/managers/queueManager.ts @@ -6,7 +6,7 @@ import { Server as SocketServer } from 'socket.io' import config from '#application/config' import Logger, { LoggerType } from '#application/logger' -import { getAppPath } from '#application/storage' +import Storage from '#application/storage' import { TSocket } from '#application/types' import SocketManager from '#managers/socketManager' @@ -56,9 +56,9 @@ class QueueManager { const { jobName, params, socketId } = job.data try { - const jobsDir = getAppPath('jobs') + const jobsDir = Storage.getAppPath('jobs') const extension = config.ENV === 'development' ? '.ts' : '.js' - const jobPath = getAppPath('jobs', `${jobName}${extension}`) + const jobPath = Storage.getAppPath('jobs', `${jobName}${extension}`) if (!fs.existsSync(jobPath)) { this.logger.warn(`Job file not found: ${jobPath}`) diff --git a/src/managers/socketManager.ts b/src/managers/socketManager.ts index bb8842a..266f8c2 100644 --- a/src/managers/socketManager.ts +++ b/src/managers/socketManager.ts @@ -7,7 +7,7 @@ import { Server as SocketServer } from 'socket.io' import config from '#application/config' import Logger, { LoggerType } from '#application/logger' -import { getAppPath } from '#application/storage' +import Storage from '#application/storage' import { TSocket } from '#application/types' import { Authentication } from '#middleware/authentication' @@ -61,11 +61,11 @@ class SocketManager { */ private async loadEventHandlers(baseDir: string, subDir: string, socket: TSocket): Promise { try { - const fullDir = getAppPath(baseDir, subDir) + const fullDir = Storage.getAppPath(baseDir, subDir) const files = await fs.promises.readdir(fullDir, { withFileTypes: true }) for (const file of files) { - const filePath = getAppPath(baseDir, subDir, file.name) + const filePath = Storage.getAppPath(baseDir, subDir, file.name) if (file.isDirectory()) { await this.loadEventHandlers(baseDir, `${subDir}/${file.name}`, socket)