1
0
forked from noxious/server

Storage class is now OOP

This commit is contained in:
Dennis Postma 2025-01-01 21:34:23 +01:00
parent 04e081c31a
commit 5982422e04
15 changed files with 109 additions and 72 deletions

View File

@ -3,7 +3,7 @@ import * as path from 'path'
import { pathToFileURL } from 'url' import { pathToFileURL } from 'url'
import Logger, { LoggerType } from '#application/logger' import Logger, { LoggerType } from '#application/logger'
import { getAppPath } from '#application/storage' import Storage from '#application/storage'
import { Command } from '#application/types' import { Command } from '#application/types'
export class CommandRegistry { export class CommandRegistry {
@ -15,7 +15,7 @@ export class CommandRegistry {
} }
public async loadCommands(): Promise<void> { public async loadCommands(): Promise<void> {
const directory = getAppPath('commands') const directory = Storage.getAppPath('commands')
this.logger.info(`Loading commands from: ${directory}`) this.logger.info(`Loading commands from: ${directory}`)
try { try {
@ -32,7 +32,7 @@ export class CommandRegistry {
private async loadCommandFile(file: fs.Dirent): Promise<void> { private async loadCommandFile(file: fs.Dirent): Promise<void> {
try { 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 commandName = path.basename(file.name, path.extname(file.name))
const module = await import(pathToFileURL(filePath).href) const module = await import(pathToFileURL(filePath).href)

View File

@ -1,34 +1,71 @@
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import config from './config' import config from '#application/config'
export function getRootPath(folder: string, ...additionalSegments: string[]) { class Storage {
return path.join(process.cwd(), folder, ...additionalSegments) private readonly baseDir: string
} private readonly rootDir: string
export function getAppPath(folder: string, ...additionalSegments: string[]) { constructor() {
const baseDir = config.ENV === 'development' ? 'src' : 'dist' this.rootDir = process.cwd()
return path.join(process.cwd(), baseDir, folder, ...additionalSegments) 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)
}
/**
* 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')
}
export function doesPathExist(path: string) {
try { try {
fs.accessSync(path, fs.constants.F_OK) fs.accessSync(pathToCheck, fs.constants.F_OK)
return true return true
} catch (e) { } catch (e) {
return false 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')
}
export function createDir(path: string) {
try { try {
fs.mkdirSync(path, { recursive: true }) fs.mkdirSync(dirPath, { recursive: true })
} catch (e) { } catch (error) {
console.error(e) const typedError = error as Error
throw new Error(`Failed to create directory: ${typedError.message}`)
}
} }
} }
export default new Storage()

View File

@ -1,11 +1,10 @@
import fs from 'fs' import fs from 'fs'
import sharp from 'sharp' import sharp from 'sharp'
import { Server } from 'socket.io'
import { BaseCommand } from '#application/base/baseCommand' import { BaseCommand } from '#application/base/baseCommand'
import { CharacterGender, CharacterRace } from '#application/enums' import { CharacterGender, CharacterRace } from '#application/enums'
import { getPublicPath } from '#application/storage' import Storage from '#application/storage'
import { UUID } from '#application/types' import { UUID } from '#application/types'
import { Character } from '#entities/character' import { Character } from '#entities/character'
import { CharacterHair } from '#entities/characterHair' import { CharacterHair } from '#entities/characterHair'
@ -44,7 +43,7 @@ export default class InitCommand extends BaseCommand {
} }
private async importTiles(): Promise<void> { private async importTiles(): Promise<void> {
for (const tile of fs.readdirSync(getPublicPath('tiles'))) { for (const tile of fs.readdirSync(Storage.getPublicPath('tiles'))) {
const newTile = new Tile() const newTile = new Tile()
newTile.setId(tile.split('.')[0] as UUID).setName('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<void> { private async importObjects(): Promise<void> {
for (const object of fs.readdirSync(getPublicPath('objects'))) { for (const object of fs.readdirSync(Storage.getPublicPath('objects'))) {
const newMapObject = new MapObject() const newMapObject = new MapObject()
newMapObject newMapObject
.setId(object.split('.')[0] as UUID) .setId(object.split('.')[0] as UUID)
.setName('New object') .setName('New object')
.setFrameWidth( .setFrameWidth(
(await sharp(getPublicPath('objects', object)) (await sharp(Storage.getPublicPath('objects', object))
.metadata() .metadata()
.then((metadata) => metadata.height)) ?? 0 .then((metadata) => metadata.height)) ?? 0
) )
.setFrameHeight( .setFrameHeight(
(await sharp(getPublicPath('objects', object)) (await sharp(Storage.getPublicPath('objects', object))
.metadata() .metadata()
.then((metadata) => metadata.width)) ?? 0 .then((metadata) => metadata.width)) ?? 0
) )

View File

@ -3,12 +3,12 @@ import fs from 'fs'
import sharp from 'sharp' import sharp from 'sharp'
import { BaseCommand } from '#application/base/baseCommand' import { BaseCommand } from '#application/base/baseCommand'
import { getPublicPath } from '#application/storage' import Storage from '#application/storage'
export default class TilesCommand extends BaseCommand { export default class TilesCommand extends BaseCommand {
public async execute(): Promise<void> { public async execute(): Promise<void> {
// Get all tiles // Get all tiles
const tilesDir = getPublicPath('tiles') const tilesDir = Storage.getPublicPath('tiles')
const tiles = fs.readdirSync(tilesDir).filter((file) => file.endsWith('.png')) const tiles = fs.readdirSync(tilesDir).filter((file) => file.endsWith('.png'))
// Create output directory if it doesn't exist // Create output directory if it doesn't exist
@ -18,14 +18,14 @@ export default class TilesCommand extends BaseCommand {
for (const tile of tiles) { for (const tile of tiles) {
// Check if tile is already 66x34 // 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) { if (metadata.width === 66 && metadata.height === 34) {
this.logger.info(`Tile ${tile} already processed`) this.logger.info(`Tile ${tile} already processed`)
continue continue
} }
const inputPath = getPublicPath('tiles', tile) const inputPath = Storage.getPublicPath('tiles', tile)
const tempPath = getPublicPath('tiles', `temp_${tile}`) const tempPath = Storage.getPublicPath('tiles', `temp_${tile}`)
try { try {
await sharp(inputPath) await sharp(inputPath)

View File

@ -4,7 +4,7 @@ import { Server } from 'socket.io'
import { gameLogger, gameMasterLogger } from '#application/logger' import { gameLogger, gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma' import prisma from '#application/prisma'
import { getPublicPath } from '#application/storage' import Storage from '#application/storage'
import { TSocket } from '#application/types' import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository' import characterRepository from '#repositories/characterRepository'
@ -38,10 +38,10 @@ export default class ObjectRemoveEvent {
}) })
// get root path // get root path
const public_folder = getPublicPath('objects') const public_folder = Storage.getPublicPath('objects')
// remove the tile from the disk // 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) => { fs.unlink(finalFilePath, (err) => {
if (err) { if (err) {
gameMasterLogger.error(`Error deleting object ${data.object}: ${err.message}`) gameMasterLogger.error(`Error deleting object ${data.object}: ${err.message}`)

View File

@ -6,7 +6,7 @@ import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger' import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma' import prisma from '#application/prisma'
import { getPublicPath } from '#application/storage' import Storage from '#application/storage'
import { TSocket } from '#application/types' import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository' import characterRepository from '#repositories/characterRepository'
@ -32,7 +32,7 @@ export default class ObjectUploadEvent {
if (character.role !== 'gm') { if (character.role !== 'gm') {
return callback(false) return callback(false)
} }
const public_folder = getPublicPath('objects') const public_folder = Storage.getPublicPath('objects')
// Ensure the folder exists // Ensure the folder exists
await fs.mkdir(public_folder, { recursive: true }) await fs.mkdir(public_folder, { recursive: true })
@ -56,7 +56,7 @@ export default class ObjectUploadEvent {
const uuid = object.id const uuid = object.id
const filename = `${uuid}.png` const filename = `${uuid}.png`
const finalFilePath = getPublicPath('objects', filename) const finalFilePath = Storage.getPublicPath('objects', filename)
await writeFile(finalFilePath, objectData) await writeFile(finalFilePath, objectData)
gameMasterLogger.info('gm:object:upload', `Object ${key} uploaded with id ${uuid}`) gameMasterLogger.info('gm:object:upload', `Object ${key} uploaded with id ${uuid}`)

View File

@ -3,7 +3,7 @@ import fs from 'fs/promises'
import { Server } from 'socket.io' import { Server } from 'socket.io'
import prisma from '#application/prisma' import prisma from '#application/prisma'
import { getPublicPath } from '#application/storage' import Storage from '#application/storage'
import { TSocket } from '#application/types' import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository' import characterRepository from '#repositories/characterRepository'
@ -26,7 +26,7 @@ export default class SpriteCreateEvent {
return callback(false) return callback(false)
} }
const public_folder = getPublicPath('sprites') const public_folder = Storage.getPublicPath('sprites')
// Ensure the folder exists // Ensure the folder exists
await fs.mkdir(public_folder, { recursive: true }) await fs.mkdir(public_folder, { recursive: true })
@ -39,7 +39,7 @@ export default class SpriteCreateEvent {
const uuid = sprite.id const uuid = sprite.id
// Create folder with uuid // Create folder with uuid
const sprite_folder = getPublicPath('sprites', uuid) const sprite_folder = Storage.getPublicPath('sprites', uuid)
await fs.mkdir(sprite_folder, { recursive: true }) await fs.mkdir(sprite_folder, { recursive: true })
callback(true) callback(true)

View File

@ -4,7 +4,7 @@ import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger' import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma' import prisma from '#application/prisma'
import { getPublicPath } from '#application/storage' import Storage from '#application/storage'
import { TSocket } from '#application/types' import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository' import CharacterRepository from '#repositories/characterRepository'
@ -19,7 +19,7 @@ export default class GMSpriteDeleteEvent {
private readonly io: Server, private readonly io: Server,
private readonly socket: TSocket private readonly socket: TSocket
) { ) {
this.public_folder = getPublicPath('sprites') this.public_folder = Storage.getPublicPath('sprites')
} }
public listen(): void { public listen(): void {
@ -45,7 +45,7 @@ export default class GMSpriteDeleteEvent {
} }
private async deleteSpriteFolder(spriteId: string): Promise<void> { private async deleteSpriteFolder(spriteId: string): Promise<void> {
const finalFilePath = getPublicPath('sprites', spriteId) const finalFilePath = Storage.getPublicPath('sprites', spriteId)
if (fs.existsSync(finalFilePath)) { if (fs.existsSync(finalFilePath)) {
await fs.promises.rmdir(finalFilePath, { recursive: true }) await fs.promises.rmdir(finalFilePath, { recursive: true })

View File

@ -7,7 +7,7 @@ import type { Prisma, SpriteAction } from '@prisma/client'
import { gameMasterLogger } from '#application/logger' import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma' import prisma from '#application/prisma'
import { getPublicPath } from '#application/storage' import Storage from '#application/storage'
import { TSocket } from '#application/types' import { TSocket } from '#application/types'
import CharacterRepository from '#repositories/characterRepository' import CharacterRepository from '#repositories/characterRepository'
@ -314,13 +314,13 @@ export default class SpriteUpdateEvent {
} }
private async saveSpritesToDisk(id: string, actions: ProcessedSpriteAction[]): Promise<void> { private async saveSpritesToDisk(id: string, actions: ProcessedSpriteAction[]): Promise<void> {
const publicFolder = getPublicPath('sprites', id) const publicFolder = Storage.getPublicPath('sprites', id)
await mkdir(publicFolder, { recursive: true }) await mkdir(publicFolder, { recursive: true })
await Promise.all( await Promise.all(
actions.map(async (action) => { actions.map(async (action) => {
const spritesheet = await this.createSpritesheet(action.buffersWithDimensions) 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)
}) })
) )
} }

View File

@ -4,7 +4,7 @@ import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger' import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma' import prisma from '#application/prisma'
import { getPublicPath } from '#application/storage' import Storage from '#application/storage'
import { TSocket } from '#application/types' import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository' import characterRepository from '#repositories/characterRepository'
@ -19,7 +19,7 @@ export default class GMTileDeleteEvent {
private readonly io: Server, private readonly io: Server,
private readonly socket: TSocket private readonly socket: TSocket
) { ) {
this.public_folder = getPublicPath('tiles') this.public_folder = Storage.getPublicPath('tiles')
} }
public listen(): void { public listen(): void {
@ -56,7 +56,7 @@ export default class GMTileDeleteEvent {
} }
private async deleteTileFile(tileId: string): Promise<void> { private async deleteTileFile(tileId: string): Promise<void> {
const finalFilePath = getPublicPath('tiles', `${tileId}.png`) const finalFilePath = Storage.getPublicPath('tiles', `${tileId}.png`)
try { try {
await fs.unlink(finalFilePath) await fs.unlink(finalFilePath)
} catch (error: any) { } catch (error: any) {

View File

@ -5,7 +5,7 @@ import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger' import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma' import prisma from '#application/prisma'
import { getPublicPath } from '#application/storage' import Storage from '#application/storage'
import { TSocket } from '#application/types' import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository' import characterRepository from '#repositories/characterRepository'
@ -32,7 +32,7 @@ export default class TileUploadEvent {
return return
} }
const public_folder = getPublicPath('tiles') const public_folder = Storage.getPublicPath('tiles')
// Ensure the folder exists // Ensure the folder exists
await fs.mkdir(public_folder, { recursive: true }) await fs.mkdir(public_folder, { recursive: true })
@ -45,7 +45,7 @@ export default class TileUploadEvent {
}) })
const uuid = tile.id const uuid = tile.id
const filename = `${uuid}.png` const filename = `${uuid}.png`
const finalFilePath = getPublicPath('tiles', filename) const finalFilePath = Storage.getPublicPath('tiles', filename)
await writeFile(finalFilePath, tileData) await writeFile(finalFilePath, tileData)
}) })

View File

@ -4,7 +4,7 @@ import { Request, Response } from 'express'
import { BaseController } from '#application/base/baseController' import { BaseController } from '#application/base/baseController'
import Database from '#application/database' import Database from '#application/database'
import { getPublicPath } from '#application/storage' import Storage from '#application/storage'
import { AssetData, UUID } from '#application/types' import { AssetData, UUID } from '#application/types'
import SpriteRepository from '#repositories/spriteRepository' import SpriteRepository from '#repositories/spriteRepository'
import TileRepository from '#repositories/tileRepository' import TileRepository from '#repositories/tileRepository'
@ -98,7 +98,7 @@ export class AssetsController extends BaseController {
public async downloadAsset(req: Request, res: Response) { public async downloadAsset(req: Request, res: Response) {
const { type, spriteId, file } = req.params 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)) { if (!fs.existsSync(assetPath)) {
this.logger.error(`File not found: ${assetPath}`) this.logger.error(`File not found: ${assetPath}`)

View File

@ -4,14 +4,15 @@ import { Request, Response } from 'express'
import sharp from 'sharp' import sharp from 'sharp'
import { BaseController } from '#application/base/baseController' 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 CharacterHairRepository from '#repositories/characterHairRepository'
import CharacterRepository from '#repositories/characterRepository' import CharacterRepository from '#repositories/characterRepository'
import CharacterTypeRepository from '#repositories/characterTypeRepository' import CharacterTypeRepository from '#repositories/characterTypeRepository'
interface AvatarOptions { interface AvatarOptions {
characterTypeId: number characterTypeId: UUID
characterHairId?: number characterHairId?: UUID
} }
export class AvatarController extends BaseController { export class AvatarController extends BaseController {
@ -57,7 +58,7 @@ export class AvatarController extends BaseController {
return this.sendError(res, 'Character type not found', 404) 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)) { if (!fs.existsSync(bodySpritePath)) {
return this.sendError(res, 'Body sprite file not found', 404) return this.sendError(res, 'Body sprite file not found', 404)
} }
@ -71,7 +72,7 @@ export class AvatarController extends BaseController {
if (options.characterHairId) { if (options.characterHairId) {
const characterHair = await CharacterHairRepository.getById(options.characterHairId) const characterHair = await CharacterHairRepository.getById(options.characterHairId)
if (characterHair?.sprite?.id) { 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)) { if (fs.existsSync(hairSpritePath)) {
avatar = avatar.composite([{ input: hairSpritePath, gravity: 'north' }]) avatar = avatar.composite([{ input: hairSpritePath, gravity: 'north' }])
} }

View File

@ -6,7 +6,7 @@ import { Server as SocketServer } from 'socket.io'
import config from '#application/config' import config from '#application/config'
import Logger, { LoggerType } from '#application/logger' import Logger, { LoggerType } from '#application/logger'
import { getAppPath } from '#application/storage' import Storage from '#application/storage'
import { TSocket } from '#application/types' import { TSocket } from '#application/types'
import SocketManager from '#managers/socketManager' import SocketManager from '#managers/socketManager'
@ -56,9 +56,9 @@ class QueueManager {
const { jobName, params, socketId } = job.data const { jobName, params, socketId } = job.data
try { try {
const jobsDir = getAppPath('jobs') const jobsDir = Storage.getAppPath('jobs')
const extension = config.ENV === 'development' ? '.ts' : '.js' const extension = config.ENV === 'development' ? '.ts' : '.js'
const jobPath = getAppPath('jobs', `${jobName}${extension}`) const jobPath = Storage.getAppPath('jobs', `${jobName}${extension}`)
if (!fs.existsSync(jobPath)) { if (!fs.existsSync(jobPath)) {
this.logger.warn(`Job file not found: ${jobPath}`) this.logger.warn(`Job file not found: ${jobPath}`)

View File

@ -7,7 +7,7 @@ import { Server as SocketServer } from 'socket.io'
import config from '#application/config' import config from '#application/config'
import Logger, { LoggerType } from '#application/logger' import Logger, { LoggerType } from '#application/logger'
import { getAppPath } from '#application/storage' import Storage from '#application/storage'
import { TSocket } from '#application/types' import { TSocket } from '#application/types'
import { Authentication } from '#middleware/authentication' import { Authentication } from '#middleware/authentication'
@ -61,11 +61,11 @@ class SocketManager {
*/ */
private async loadEventHandlers(baseDir: string, subDir: string, socket: TSocket): Promise<void> { private async loadEventHandlers(baseDir: string, subDir: string, socket: TSocket): Promise<void> {
try { try {
const fullDir = getAppPath(baseDir, subDir) const fullDir = Storage.getAppPath(baseDir, subDir)
const files = await fs.promises.readdir(fullDir, { withFileTypes: true }) const files = await fs.promises.readdir(fullDir, { withFileTypes: true })
for (const file of files) { for (const file of files) {
const filePath = getAppPath(baseDir, subDir, file.name) const filePath = Storage.getAppPath(baseDir, subDir, file.name)
if (file.isDirectory()) { if (file.isDirectory()) {
await this.loadEventHandlers(baseDir, `${subDir}/${file.name}`, socket) await this.loadEventHandlers(baseDir, `${subDir}/${file.name}`, socket)