UNPKG

mcbe-portal

Version:

Connect bedrock servers together using mcbe-portal, manage your servers with ease.

289 lines (230 loc) 9.89 kB
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 };