import type { FSHeader, IEndpointFS } from "$lib/server/broker-utils/FileSystem/endpoints/endpoints" import { NGINX_BASE } from "$lib/server/utils/constants" import { doesFileExist, isDir, loadFile } from "$lib/server/utils/filesystem-utils" import { watch, type FSWatcher } from "node:fs" import { parseConf } from "./utils" import { logger } from "$lib/server/utils/logger" import { EndpointType } from "$lib/server/enums/endpoints" export class EndpointBrokerManagerFS { private static initialized = false private static watcher: FSWatcher /** Here we store all endpoints * - Key: path * - Value: any IEndpointFS */ private static endpoints: Map private static usedPorts: number[] private static lastNginxReload: Date = new Date() public static get ready() { return EndpointBrokerManagerFS.initialized } public static init() { // TODO: Read all files // TODO: QUICK parse them // TODO: Initialize a file watcher } public static async createEndpoint(endpoint: IEndpointFS) { } public static async changeEndpoint(path: string, newEndpoint: IEndpointFS) { } public static async activateEndpoint(path: string): Promise { return true } public static async deactivateEndpoint(path: string): Promise { return true } public static getEndpointByPath(path: string): IEndpointFS | null { const endpoint = EndpointBrokerManagerFS.endpoints.get( path ) if (!endpoint) { return null } return endpoint } public static async getAll(): Promise { return Array.from(EndpointBrokerManagerFS.endpoints.values()) } // MARK: private methods /** * This method is the only one that adds or removes files from * our manager. No other method should mess up with the mapping * of the manager * * @returns FSWatcher, to avoid having it garbage collected */ private static watchNginxDirectory(): FSWatcher { const OPTIONS = { recursive: true } const WATCHER = watch(NGINX_BASE, OPTIONS, async (eventType, filename) => { const RELATIVE_PATH = filename const FULL_PATH = `${NGINX_BASE}/${RELATIVE_PATH}` // TODO: check if it's a directory, if so, skip if (await isDir(FULL_PATH)) { return } // UGLY: there may be race conditions, rarely, but // UGLY: there may be // UGLY: probably solved // TODO: Find a way to lock files // TODO: probably solved switch (eventType) { case "change": { const oldEndpoint = EndpointBrokerManagerFS.endpoints.get( FULL_PATH ) if (!oldEndpoint) { logger.debug(`File changed but was never tracked\nPATH: ${FULL_PATH}`, "EP Manager") return } // Nothing to do, it's not managed by us if(oldEndpoint.type === EndpointType.MANUAL) { return } const file = await loadFile(FULL_PATH) await file.lock() // NOTE: HERE USE FILE const newHash = await file.hash() const oldHash = oldEndpoint.hash if (newHash === oldHash) { // Files are equal // or we are very unlucky await file.release() return } // NOTE: HERE USE FILE const stats = await file.getStats() const hash = await file.hash() const conf = await file.text() const fsHeader: FSHeader = { name: filename!.split("/").pop()!, stats: stats, path: FULL_PATH, hash: hash } const newEndpoint = parseConf(fsHeader, conf) // Check if files are trying to represent // the same endpoint if (oldEndpoint.headerHash() !== newEndpoint.headerHash()) { // Files are not equal // or we are very unlucky await file.release() return } // Endpoints are different, but changes were never // coming from here logger.debug( "Corrupted file detected", "EP Manager Corruption Detected" ) await file.write(oldEndpoint.toConf()) await file.release() // NOTE: HERE DO NOT USE FILE break } // Basically it's rename default: { const isNew = await doesFileExist(FULL_PATH) if (!isNew) { // This means that the file doesn't exist // let's just acknowledge this // UGLY: not checking for false values // UGLY: hints that something went wrong EndpointBrokerManagerFS.endpoints.delete( FULL_PATH ) } // Technically the file is new const file = await loadFile(FULL_PATH) await file.lock() // NOTE: HERE USE FILE const stats = await file.getStats() const hash = await file.hash() const conf = await file.text() await file.release() // NOTE: HERE DO NOT USE FILE // UGLY: forcing typecasting const fsHeader: FSHeader = { name: filename!.split("/").pop()!, stats: stats, path: FULL_PATH, hash: hash } const endpoint = parseConf(fsHeader, conf) EndpointBrokerManagerFS.endpoints.set( FULL_PATH, endpoint ) return } } }) return WATCHER } }