import type { FSHeader, IEndpointFS } from "$lib/server/broker-utils/FileSystem/endpoints/endpoints" import { NGINX_ACTIVE, NGINX_BASE, NGINX_INACTIVE, NGINX_TRACKED } from "$lib/server/utils/constants" import { doesFileExist, isDir, listFiles, 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 { EndpointStatus, EndpointType } from "$lib/server/enums/endpoints" import { validatePath } from "$lib/shared/utils/path-utils" 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 async init() { if (EndpointBrokerManagerFS.ready) { // UGLY: be specific throw new Error("Broker already initialized") } const configurations = (await listFiles(NGINX_TRACKED, true)).filter( async (path) => { if (await isDir(path)) { return false } if (!path.endsWith(".conf")) { return false } return true }) for (const confPath of configurations) { const file = await loadFile(confPath) await file.lock() const conf = await file.text() const fileStats = await file.getStats() const fsHeader: FSHeader = { name: confPath.split("/").pop()!, stats: fileStats, path: confPath, hash: await file.hash() } await file.release() const endpoint = await parseConf(fsHeader, conf) const relativePath = confPath.replace(NGINX_TRACKED, "") EndpointBrokerManagerFS.endpoints.set( relativePath, endpoint ) } // TODO: Initialize a file watcher EndpointBrokerManagerFS.watcher = EndpointBrokerManagerFS.watchNginxDirectory() EndpointBrokerManagerFS.initialized = true } public static async createEndpoint(endpoint: IEndpointFS) { if (endpoint.type === EndpointType.MANUAL) { // UGLY: be specific throw new Error("You can't create a manual conf automatically") } const REAL_PATH = `${NGINX_TRACKED}/${endpoint.path}` if (await doesFileExist(REAL_PATH) ) { // UGLY: be specific throw new Error("File already existant") } const file = await loadFile(REAL_PATH, true) await file.lock() await file.write(endpoint.toConf()) await file.release() } public static async deleteEndpoint(path: string) { validatePath(path) const REAL_PATH = `${NGINX_TRACKED}/${path}` if (! await doesFileExist(REAL_PATH)) { // UGLY: be specific throw new Error("This path does not exist") } const file = await loadFile(REAL_PATH) await file.delete() const endpoint = EndpointBrokerManagerFS.endpoints.get( path ) EndpointBrokerManagerFS.endpoints.delete( path ) return endpoint } public static async changeEndpoint(path: string, newEndpoint: IEndpointFS) { if (newEndpoint.type === EndpointType.MANUAL) { // UGLY: be specific throw new Error("Change of Manual endpoint is not supported yet") } validatePath(path) const REAL_PATH = `${NGINX_TRACKED}/${path}` if (! await doesFileExist(REAL_PATH)) { // UGLY: more specific throw new Error("The requested file does not exist") } const epFile = await loadFile(REAL_PATH) await epFile.lock() await epFile.write( newEndpoint.toConf() ) await epFile.release() EndpointBrokerManagerFS.endpoints.set(path, newEndpoint) const status = await EndpointBrokerManagerFS.getStatus(path) if (status === EndpointStatus.ACTIVE) { await this.activateEndpoint(path) } } public static async activateEndpoint(path: string): Promise { validatePath(path) const endpoint = EndpointBrokerManagerFS.getEndpointByPath(path) if (!endpoint) { // UGLY: be specific throw new Error("This specified endpoint doesn't exist") } const activePath = `${NGINX_ACTIVE}/${path}` if (! await doesFileExist(activePath)) { const file = await loadFile(activePath, true) await file.write( endpoint.toConf() ) return true } const file = await loadFile(activePath) await file.lock() const fileHash = await file.hash() if (endpoint.hash === fileHash) { await file.release() return false } await file.write( endpoint.toConf() ) await file.release() return true } public static async deactivateEndpoint(path: string): Promise { validatePath(path) const endpoint = EndpointBrokerManagerFS.getEndpointByPath(path) if (!endpoint) { // UGLY: be specific throw new Error("This specified endpoint doesn't exist") } const activePath = `${NGINX_ACTIVE}/${path}` if (! await doesFileExist(activePath)) { return false } const file = await loadFile(activePath) await file.delete() return true } public static getEndpointByPath(path: string): IEndpointFS | null { validatePath(path) const endpoint = EndpointBrokerManagerFS.endpoints.get( path ) if (!endpoint) { return null } return endpoint } public static async getAll(): Promise { return Array.from(EndpointBrokerManagerFS.endpoints.values()) } public static async getStatus(path: string) { validatePath(path) const REAL_ACTIVE_PATH = `${NGINX_ACTIVE}/${path}` const REAL_TRACKED_PATH = `${NGINX_TRACKED}/${path}` if (await doesFileExist(REAL_ACTIVE_PATH)) { return EndpointStatus.ACTIVE } if (await doesFileExist(REAL_TRACKED_PATH)) { return EndpointStatus.INACTIVE } // UGLY: more specific throw new Error("This path is non existant") } // 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 NGINX_TRACK = NGINX_TRACKED const WATCHER = watch(NGINX_TRACK, OPTIONS, async (eventType, filename) => { if (!filename) { return } const RELATIVE_PATH = filename const FULL_PATH = `${NGINX_TRACK}/${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( RELATIVE_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( RELATIVE_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( RELATIVE_PATH, endpoint ) return } } }) return WATCHER } }