mcbe-portal
Version:
Connect bedrock servers together using mcbe-portal, manage your servers with ease.
289 lines (230 loc) • 9.89 kB
JavaScript
const { spawn } = require("node:child_process");
const path = require("node:path");
const https = require("node:https");
const extract = require("extract-zip");
const os = require("node:os");
const fs = require("node:fs");
const { latesetURL, OSMap } = require("./internals");
/** - Configuration for mcbe-portal backups.
* @typedef {Object} PortalConfigBackups
* @property {true|false} enabled - Enable backups.
* @property {Array<String>} active - List of server names to backup.
* @property {Number} interval - Interval for backing up your server.
*/
/** - Configuration for mcbe-portal.
* @typedef {Object} PortalConfig
* @property {PortalConfigBackups} backups - Backup configuration.
* @property {PortalConfigDatabase} database - Database configuration.
* @property {PortalConfigPacks} packs - Pack configuration.
* @property {String} prefix - Prefix configuration for console commands.
*/
/** - Configuration for mcbe-portal database.
* @typedef {Object} PortalConfigDatabase
* @property {true|false} enabled - Enabled the Database.
*/
/** - Configuration for mcbe-portal server packs.
* @typedef {Object} PortalConfigPacks
* @property {true|false} enabled - Enable Cross Packs.
* @property {Array<String>} uuids - List of pack UUIDS.
* @property {Array<String>} exclude - List of server names to exclude from pack progression.
*/
class Portal {
/** - MCBE-PORTAL Configuration
* @param {PortalConfig} config
*/
constructor(config) {
require("./readline")(this);
process.on("SIGINT", () => {
console.log("\nShutting down servers...");
this.stop_servers();
});
this.config = {
backups: {enabled: false, active: [], interval: (12 * 60 * 60 * 1000)},
database: {enabled: false},
packs: {enabled: false, uuids: [], exclude:[]},
plugins: [],
prefix: "-",
...config
};
this.servers = [];
this.backupInterval = null;
}
update_config(config) {
if (this.backupInterval) {
clearInterval(this.backupInterval);
this.backupInterval = null;
}
this.config = {
...this.config,
...config
};
this.backup_servers();
}
restart_servers () {
const servers = this.servers;
this.stop_servers();
console.clear();
console.log("Restarting all servers...");
setTimeout(() => {
servers.forEach((server) => {
this.create_server(server.name, server.path)
})
}, 2000);
}
async create_server(name, filePath) {
let latest_version;
const platform = os.platform();
// If OS !== windows use bedrock_server as linux doesnt use .exe
const serverBinary = platform === "win32" ? "bedrock_server.exe" : "bedrock_server";
const serverPath = path.join(filePath, serverBinary);
const mappedOS = OSMap[platform];
if (!mappedOS) throw new Error(`Unsupported OS: ${platform}`);
await latesetURL(mappedOS).then(url => {
latest_version = url.match(/bedrock-server-([\d.]+)\.zip/)[1];
});
if (!fs.existsSync(filePath)) {
fs.mkdirSync(filePath, { recursive: true });
}
// If server files are not provided, mcbe-portal creates them for you
if (!fs.existsSync(serverPath)) {
console.log("Bedrock server not found. Downloading...");
await this.#download_bedrock(filePath, latest_version);
}
this.run_server(name, filePath);
}
plugins(plugs) {
plugs.forEach((plugin) => {
plugin[0](...plugin[1]);
});
}
run_server(name, filePath) {
const serverProcess = spawn("./bedrock_server", { cwd: filePath, stdio: ["pipe", "pipe", "pipe"] });
this.servers.push({ name, process: serverProcess, path: filePath });
serverProcess.stdout.on("data", (data) => {
console.log(data.toString("utf-8"));
});
this.backup_servers();
this.deploy_scripts();
}
async #download_bedrock(filePath, version) {
const platform = os.platform();
const osKey = OSMap[platform];
if (!osKey) throw new Error(`Unsupported OS: ${platform}`);
const url = `https://www.minecraft.net/bedrockdedicatedserver/bin-${osKey}/bedrock-server-${version}.zip`;
const resolvedPath = path.resolve(filePath);
const zipPath = path.join(resolvedPath, `bedrock-server-${version}.zip`);
fs.mkdirSync(resolvedPath, { recursive: true });
return new Promise((resolve, reject) => {
https.get(url, (response) => {
if (response.statusCode !== 200) {
return reject(new Error(`Failed to download server version ${version} - HTTP ${response.statusCode}`));
}
const file = fs.createWriteStream(zipPath);
response.pipe(file);
file.on("finish", async () => {
file.close();
try {
await extract(zipPath, { dir: resolvedPath });
fs.unlinkSync(zipPath);
resolve();
} catch (err) {
reject(err);
}
});
file.on("error", reject);
}).on("error", reject);
});
}
broadcast_command(command) {
this.servers.forEach(({ process: pro }) => {
if (pro.stdin.writable) {
pro.stdin.write(command + "\n");
if (command === "stop") {
this.stop_servers();
return process.exit(1);
}
}
});
}
stop_servers() {
this.servers.forEach((server) => {
server.process.kill();
});
this.servers = [];
}
backup_servers() {
const backupDir = path.join(process.cwd(), "backups");
if (!this.config.backups.enabled) return;
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
if (this.config.backups.enabled) {
if (!this.backupInterval) {
this.backupInterval = setInterval(() => {
this.backup_servers();
}, this.config.backups.interval || 12 * 60 * 60 * 1000);
}
}
this.servers.forEach(({ name, path: filePath }) => {
if (!this.config.backups.active.includes(name)) return;
const serverBackupDir = path.join(backupDir, name);
if (!fs.existsSync(serverBackupDir)) {
fs.mkdirSync(serverBackupDir, { recursive: true });
}
const timestamp = new Date().toLocaleString('en-GB', {
year: '2-digit',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hourCycle: 'h23'
}).replaceAll("/", "-").replaceAll(":", "-").replaceAll(", ", "-").toString();
const serverBackupPath = path.join(serverBackupDir, timestamp);
fs.mkdirSync(serverBackupPath, { recursive: true });
const worldDir = path.join(filePath, "worlds");
if (fs.existsSync(worldDir)) {
fs.cpSync(worldDir, path.join(serverBackupPath, "worlds"), { recursive: true });
console.log(`Backed up ${name} server world files to ${serverBackupPath}`);
} else {
console.log(`World folder not found for ${name}`);
}
});
}
deploy_scripts() {
if (!this.config.packs.enabled) return;
let dirs = {
0: path.join(process.cwd(), "packs")
}
dirs[1] = path.join(dirs[0], "behavior");
dirs[2] = path.join(dirs[0], "resource");
[dirs[1], dirs[2]].forEach((dir) => {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
});
[dirs[1], dirs[2]].forEach((pd1) => {
const scriptFolders = fs.readdirSync(pd1);
scriptFolders.forEach((pack) => {
const scriptPath = path.join(pd1, pack);
const manifestPath = path.join(scriptPath, "manifest.json");
if (fs.existsSync(manifestPath)) {
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
if (this.config.packs.uuids.includes(manifest.header.uuid)) {
this.servers.forEach(({ name, path: serverPath }) => {
if (this.config.packs.exclude.includes(name)) return;
const destFolder = pd1.includes("behavior")
? "development_behavior_packs"
: "development_resource_packs";
const destPath = path.join(serverPath, destFolder, pack);
if (fs.existsSync(destPath)) {
fs.rmSync(destPath, { recursive: true, force: true });
}
fs.cpSync(scriptPath, destPath, { recursive: true });
console.log(`Deployed ${manifest.header.uuid} to ${destFolder}`);
});
}
}
});
});
}
}
module.exports = { Portal };