monitor/core/routes/setup/post.js
2025-04-16 22:30:27 +07:00

430 lines
15 KiB
JavaScript

const modulename = 'WebServer:SetupPost';
import path from 'node:path';
import fse from 'fs-extra';
import fsp from 'node:fs/promises';
import slash from 'slash';
import { Deployer } from '@core/deployer/index';
import { validateFixServerConfig, findLikelyCFGPath } from '@lib/fxserver/fxsConfigHelper';
import got from '@lib/got';
import consoleFactory from '@lib/console';
import recipeParser from '@core/deployer/recipeParser';
import { validateTargetPath } from '@core/deployer/utils';
import { TxConfigState } from '@shared/enums';
const console = consoleFactory(modulename);
//Helper functions
const isUndefined = (x) => (x === undefined);
const getDirectories = (source) => {
return fse.readdirSync(source, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
};
const getPotentialServerDataFolders = (source) => {
try {
return getDirectories(source)
.filter((dirent) => getDirectories(path.join(source, dirent)).includes('resources'))
.map((dirent) => slash(path.join(source, dirent)) + '/');
} catch (error) {
console.verbose.warn(`Failed to find server data folder with message: ${error.message}`);
return [];
}
};
/*
NOTE: How forgiving are we:
- Ignore trailing slashes, as well as fix backslashes
- Check if its the parent folder
- Check if its inside the parent folder
- Check if its inside current folder
- Check if it contains the string `/resources`, then if its the path up to that string
- Detect config as `server.cfg` or with wrong extensions inside the Server Data Folder
FIXME: Also note that this entire file is a bit too messy, please clean it up a bit
*/
/**
* Handle all the server control actions
* FIXME: separate into validate.ts, saveDeployer.ts, and saveLocal.ts files
* FIXME: or maybe postDeployer.ts, and postLocal.ts files
* @param {object} ctx
*/
export default async function SetupPost(ctx) {
//Sanity check
if (isUndefined(ctx.params.action)) {
return ctx.utils.error(400, 'Invalid Request');
}
const action = ctx.params.action;
//Check permissions
if (!ctx.admin.testPermission('all_permissions', modulename)) {
return ctx.send({
success: false,
message: 'You need to be the admin master or have all permissions to use the setup page.',
});
}
//Ensure the correct state for the setup page
if (txManager.configState !== TxConfigState.Setup) {
return ctx.send({
success: false,
refresh: true,
});
}
//Delegate to the specific action functions
if (action == 'validateRecipeURL') {
return await handleValidateRecipeURL(ctx);
} else if (action == 'validateLocalDeployPath') {
return await handleValidateLocalDeployPath(ctx);
} else if (action == 'validateLocalDataFolder') {
return await handleValidateLocalDataFolder(ctx);
} else if (action == 'validateCFGFile') {
return await handleValidateCFGFile(ctx);
} else if (action == 'save' && ctx.request.body.type == 'popular') {
return await handleSaveDeployerImport(ctx);
} else if (action == 'save' && ctx.request.body.type == 'remote') {
return await handleSaveDeployerImport(ctx);
} else if (action == 'save' && ctx.request.body.type == 'custom') {
return await handleSaveDeployerCustom(ctx);
} else if (action == 'save' && ctx.request.body.type == 'local') {
return await handleSaveLocal(ctx);
} else {
return ctx.send({
success: false,
message: 'Unknown setup action.',
});
}
};
/**
* Handle Validation of a remote recipe/template URL
* @param {object} ctx
*/
async function handleValidateRecipeURL(ctx) {
//Sanity check
if (isUndefined(ctx.request.body.recipeURL)) {
return ctx.utils.error(400, 'Invalid Request - missing parameters');
}
const recipeURL = ctx.request.body.recipeURL.trim();
//Make request & validate recipe
try {
const recipeText = await got.get({
url: recipeURL,
timeout: { request: 4500 }
}).text();
if (typeof recipeText !== 'string') throw new Error('This URL did not return a string.');
const recipe = recipeParser(recipeText);
return ctx.send({success: true, name: recipe.name});
} catch (error) {
return ctx.send({success: false, message: `Recipe error: ${error.message}`});
}
}
/**
* Handle Validation of a remote recipe/template URL
* @param {object} ctx
*/
async function handleValidateLocalDeployPath(ctx) {
//Sanity check
if (isUndefined(ctx.request.body.deployPath)) {
return ctx.utils.error(400, 'Invalid Request - missing parameters');
}
const deployPath = slash(path.normalize(ctx.request.body.deployPath.trim()));
//Perform path checking
try {
await validateTargetPath(deployPath); // will throw if invalid
return ctx.send({success: true, message: 'Path is valid.'});
} catch (error) {
return ctx.send({success: false, message: error.message});
}
}
/**
* Handle Validation of Local (existing) Server Data Folder
* @param {object} ctx
*/
async function handleValidateLocalDataFolder(ctx) {
//Sanity check
if (isUndefined(ctx.request.body.dataFolder)) {
return ctx.utils.error(400, 'Invalid Request - missing parameters');
}
const dataFolderPath = slash(path.normalize(ctx.request.body.dataFolder.trim() + '/'));
//FIXME: replace with stuff in core/routes/settings/saveConfigs.ts > handleFxserverCard
try {
if (!fse.existsSync(path.join(dataFolderPath, 'resources'))) {
const recoveryTemplate = `The path provided is invalid. <br>
But it looks like <code>{{attempt}}</code> is correct. <br>
Do you want to use it instead?`;
//Recovery if parent folder
const attemptIsParent = path.join(dataFolderPath, '..');
if (fse.existsSync(path.join(attemptIsParent, 'resources'))) {
const message = recoveryTemplate.replace('{{attempt}}', attemptIsParent);
return ctx.send({success: false, message, suggestion: attemptIsParent});
}
//Recovery parent inside folder
const attemptOutside = getPotentialServerDataFolders(path.join(dataFolderPath, '..'));
if (attemptOutside.length >= 1) {
const message = recoveryTemplate.replace('{{attempt}}', attemptOutside[0]);
return ctx.send({success: false, message, suggestion: attemptOutside[0]});
}
//Recovery if resources
if (dataFolderPath.includes('/resources')) {
const attemptRes = dataFolderPath.split('/resources')[0];
if (fse.existsSync(path.join(attemptRes, 'resources'))) {
const message = recoveryTemplate.replace('{{attempt}}', attemptRes);
return ctx.send({success: false, message, suggestion: attemptRes});
}
}
//Recovery subfolder
const attemptInside = getPotentialServerDataFolders(dataFolderPath);
if (attemptInside.length >= 1) {
const message = recoveryTemplate.replace('{{attempt}}', attemptInside[0]);
return ctx.send({success: false, message, suggestion: attemptInside[0]});
}
//really invalid :(
throw new Error("Couldn't locate or read a resources folder inside of the path provided.");
} else {
return ctx.send({
success: true,
detectedConfig: findLikelyCFGPath(dataFolderPath),
});
}
} catch (error) {
return ctx.send({success: false, message: error.message});
}
}
/**
* Handle Validation of CFG File
* @param {object} ctx
*/
async function handleValidateCFGFile(ctx) {
//Sanity check
if (
isUndefined(ctx.request.body.dataFolder)
|| isUndefined(ctx.request.body.cfgFile)
) {
return ctx.utils.error(400, 'Invalid Request - missing parameters');
}
const dataFolderPath = slash(path.normalize(ctx.request.body.dataFolder.trim()));
const cfgFilePathNormalized = slash(path.normalize(ctx.request.body.cfgFile.trim()));
//Validate file
try {
const result = await validateFixServerConfig(cfgFilePathNormalized, dataFolderPath);
if (result.errors) {
const message = `**The file path is correct, but there are error(s) in your config file(s):**\n${result.errors}`;
return ctx.send({success: false, markdown: true, message});
} else {
return ctx.send({success: true});
}
} catch (error) {
const message = `Error:\n ${error.message}.`;
return ctx.send({success: false, message});
}
}
/**
* Handle Save settings for local server data imports
* Actions: sets serverDataPath/cfgPath, starts the server, redirect to live console
* @param {object} ctx
*/
async function handleSaveLocal(ctx) {
//Sanity check
if (
isUndefined(ctx.request.body.name)
|| isUndefined(ctx.request.body.dataFolder)
|| isUndefined(ctx.request.body.cfgFile)
) {
return ctx.utils.error(400, 'Invalid Request - missing parameters');
}
//Prepare body input
const cfg = {
name: ctx.request.body.name.trim(),
dataFolder: slash(path.normalize(ctx.request.body.dataFolder + '/')),
cfgFile: slash(path.normalize(ctx.request.body.cfgFile)),
};
//Validating Server Data Path
try {
const stat = await fsp.stat(path.join(cfg.dataFolder, 'resources'))
if (!stat.isDirectory()) {
throw new Error('not a directory');
}
} catch (error) {
let msg = error?.message ?? 'unknown error';
if (error?.code === 'ENOENT') {
msg = 'The server data folder does not exist.';
}
return ctx.send({success: false, message: `<strong>Server Data Folder error:</strong> ${msg}`});
}
//Preparing & saving config
try {
txCore.configStore.saveConfigs({
general: {
serverName: cfg.name,
},
server: {
dataPath: cfg.dataFolder,
cfgPath: cfg.cfgFile,
}
}, ctx.admin.name);
} catch (error) {
console.warn(`[${ctx.admin.name}] Error changing global/fxserver settings via setup stepper.`);
console.verbose.dir(error);
return ctx.send({
type: 'danger',
markdown: true,
message: `**Error saving the configuration file:**\n${error.message}`
});
}
//Refreshing config
txCore.cacheStore.set('deployer:recipe', 'none');
//Logging
ctx.admin.logAction('Changing global/fxserver settings via setup stepper.');
//If running (for some reason), kill it first
if (!txCore.fxRunner.isIdle) {
ctx.admin.logCommand('STOP SERVER');
await txCore.fxRunner.killServer('new server set up', ctx.admin.name, true);
}
//Starting server
const spawnError = await txCore.fxRunner.spawnServer(false);
if (spawnError !== null) {
return ctx.send({success: false, markdown: true, message: spawnError});
} else {
return ctx.send({success: true});
}
}
/**
* Handle Save settings for remote recipe importing
* Actions: download recipe, starts deployer
* @param {object} ctx
*/
async function handleSaveDeployerImport(ctx) {
//Sanity check
if (
isUndefined(ctx.request.body.name)
|| isUndefined(ctx.request.body.isTrustedSource)
|| isUndefined(ctx.request.body.recipeURL)
|| isUndefined(ctx.request.body.targetPath)
|| isUndefined(ctx.request.body.deploymentID)
) {
return ctx.utils.error(400, 'Invalid Request - missing parameters');
}
const isTrustedSource = (ctx.request.body.isTrustedSource === 'true');
const serverName = ctx.request.body.name.trim();
const recipeURL = ctx.request.body.recipeURL.trim();
const targetPath = slash(path.normalize(ctx.request.body.targetPath + '/'));
const deploymentID = ctx.request.body.deploymentID;
//Get recipe
let recipeText;
try {
recipeText = await got.get({
url: recipeURL,
timeout: { request: 4500 }
}).text();
if (typeof recipeText !== 'string') throw new Error('This URL did not return a string.');
} catch (error) {
return ctx.send({success: false, message: `Recipe download error: ${error.message}`});
}
//Preparing & saving config
try {
txCore.configStore.saveConfigs({
general: { serverName },
}, ctx.admin.name);
} catch (error) {
console.warn(`[${ctx.admin.name}] Error changing global settings via setup stepper.`);
console.verbose.dir(error);
return ctx.send({
type: 'danger',
markdown: true,
message: `**Error saving the configuration file:** ${error.message}`
});
}
ctx.admin.logAction('Changing global settings via setup stepper and started Deployer.');
//Start deployer (constructor will validate the recipe)
try {
txManager.startDeployer(recipeText, deploymentID, targetPath, isTrustedSource, {serverName});
txCore.webServer.webSocket.pushRefresh('status');
} catch (error) {
return ctx.send({success: false, message: error.message});
}
return ctx.send({success: true});
}
/**
* Handle Save settings for custom recipe
* Actions: download recipe, starts deployer
* @param {object} ctx
*/
async function handleSaveDeployerCustom(ctx) {
//Sanity check
if (
isUndefined(ctx.request.body.name)
|| isUndefined(ctx.request.body.targetPath)
|| isUndefined(ctx.request.body.deploymentID)
) {
return ctx.utils.error(400, 'Invalid Request - missing parameters');
}
const serverName = ctx.request.body.name.trim();
const targetPath = slash(path.normalize(ctx.request.body.targetPath + '/'));
const deploymentID = ctx.request.body.deploymentID;
//Preparing & saving config
try {
txCore.configStore.saveConfigs({
general: { serverName },
}, ctx.admin.name);
} catch (error) {
console.warn(`[${ctx.admin.name}] Error changing global settings via setup stepper.`);
console.verbose.dir(error);
return ctx.send({
type: 'danger',
markdown: true,
message: `**Error saving the configuration file:** ${error.message}`
});
}
ctx.admin.logAction('Changing global settings via setup stepper and started Deployer.');
//Start deployer (constructor will create the recipe template)
const customMetaData = {
author: ctx.admin.name,
serverName,
};
try {
txManager.startDeployer(false, deploymentID, targetPath, false, customMetaData);
txCore.webServer.webSocket.pushRefresh('status');
} catch (error) {
return ctx.send({success: false, message: error.message});
}
return ctx.send({success: true});
}