394 lines
11 KiB
TypeScript
Raw Normal View History

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, 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<string, IEndpointFS>
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")
}
// TODO: Read all files
// TODO: QUICK parse them
parseConf()
// 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()
EndpointBrokerManagerFS.endpoints.delete(
path
)
}
public static async changeEndpoint(path: string, newEndpoint: IEndpointFS) {
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<boolean> {
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<boolean> {
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<IEndpointFS[]> {
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: RELATIVE_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
}
}