smp-manager / backend /auto-setup.js
techedstudios
feat: Auto-download GeyserMC and Floodgate for Bedrock support
2526644
Raw History Blame Contribute Delete
9.2 kB
import net from 'net';
import fs from 'fs';
import path from 'path';
import AdmZip from 'adm-zip';
// Find a free TCP port starting at a specific port
export async function findFreePort(startPort = 25565) {
let port = startPort;
while (true) {
const isFree = await new Promise((resolve) => {
const server = net.createServer();
server.once('error', () => {
resolve(false);
});
server.once('listening', () => {
server.close();
resolve(true);
});
server.listen(port);
});
if (isFree) {
return port;
}
port++;
}
}
// Download file helper
async function downloadFile(url, destPath, progressCallback) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download from ${url}: ${response.statusText}`);
}
const contentLength = response.headers.get('content-length');
const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
const fileStream = fs.createWriteStream(destPath);
const reader = response.body.getReader();
let downloadedBytes = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
fileStream.write(value);
downloadedBytes += value.length;
if (totalBytes && progressCallback) {
const pct = Math.round((downloadedBytes / totalBytes) * 100);
progressCallback(pct);
}
}
fileStream.end();
return new Promise((resolve, reject) => {
fileStream.on('finish', resolve);
fileStream.on('error', reject);
});
}
// Download PaperMC Jar
export async function downloadPaper(version = '1.21', destDir, progressCallback) {
const apiBase = 'https://api.papermc.io/v2/projects/paper';
// Get latest build
const buildInfoResponse = await fetch(`${apiBase}/versions/${version}`);
if (!buildInfoResponse.ok) {
throw new Error(`Unsupported Minecraft version: ${version}`);
}
const buildInfo = await buildInfoResponse.json();
const builds = buildInfo.builds;
const latestBuild = builds[builds.length - 1];
// Get jar file name
const jarResponse = await fetch(`${apiBase}/versions/${version}/builds/${latestBuild}`);
const jarInfo = await jarResponse.json();
const jarName = jarInfo.downloads.application.name;
const downloadUrl = `${apiBase}/versions/${version}/builds/${latestBuild}/downloads/${jarName}`;
const destPath = path.join(destDir, 'server.jar');
fs.mkdirSync(destDir, { recursive: true });
await downloadFile(downloadUrl, destPath, progressCallback);
return destPath;
}
// Download Velocity Proxy Jar
export async function downloadVelocity(version = '3.3.0-SNAPSHOT', destDir, progressCallback) {
const apiBase = 'https://api.papermc.io/v2/projects/velocity';
let buildInfoResponse = await fetch(`${apiBase}/versions/${version}`);
if (!buildInfoResponse.ok) {
version = '3.3.0-SNAPSHOT';
buildInfoResponse = await fetch(`${apiBase}/versions/${version}`);
}
const buildInfo = await buildInfoResponse.json();
const builds = buildInfo.builds;
const latestBuild = builds[builds.length - 1];
const jarResponse = await fetch(`${apiBase}/versions/${version}/builds/${latestBuild}`);
const jarInfo = await jarResponse.json();
const jarName = jarInfo.downloads.application.name;
const downloadUrl = `${apiBase}/versions/${version}/builds/${latestBuild}/downloads/${jarName}`;
const destPath = path.join(destDir, 'velocity.jar');
fs.mkdirSync(destDir, { recursive: true });
await downloadFile(downloadUrl, destPath, progressCallback);
return destPath;
}
// Download Purpur Jar
export async function downloadPurpur(version = '1.21', destDir, progressCallback) {
const downloadUrl = `https://api.purpurmc.org/v2/purpur/${version}/latest/download`;
const destPath = path.join(destDir, 'server.jar');
fs.mkdirSync(destDir, { recursive: true });
await downloadFile(downloadUrl, destPath, progressCallback);
return destPath;
}
// Download Bedrock Dedicated Server ZIP and extract it
export async function downloadBedrock(destDir, progressCallback) {
// Use a stable, official CDN URL for Windows Bedrock Server (1.20.81.01 as default stable fallback)
const downloadUrl = 'https://minecraft.azureedge.net/bin-win/bedrock-server-1.20.81.01.zip';
const zipPath = path.join(destDir, 'bedrock-server.zip');
fs.mkdirSync(destDir, { recursive: true });
// Download the ZIP
await downloadFile(downloadUrl, zipPath, progressCallback);
// Extract using adm-zip
const zip = new AdmZip(zipPath);
zip.extractAllTo(destDir, true);
// Delete the ZIP file after extraction
fs.unlinkSync(zipPath);
return path.join(destDir, 'bedrock_server.exe');
}
// Download Fabric Server Jar
export async function downloadFabric(version = '1.21', destDir, progressCallback) {
// Get latest loader version
const loaderRes = await fetch('https://meta.fabricmc.net/v2/versions/loader');
const loaders = await loaderRes.json();
const loaderVersion = loaders[0].version;
// Get latest installer version
const installerRes = await fetch('https://meta.fabricmc.net/v2/versions/installer');
const installers = await installerRes.json();
const installerVersion = installers[0].version;
const downloadUrl = `https://meta.fabricmc.net/v2/versions/loader/${version}/${loaderVersion}/${installerVersion}/server/jar`;
const destPath = path.join(destDir, 'fabric-server-launch.jar');
fs.mkdirSync(destDir, { recursive: true });
await downloadFile(downloadUrl, destPath, progressCallback);
return destPath;
}
// Download FRPC binary
import os from 'os';
export async function downloadFrpc(destDir, progressCallback) {
const platform = os.platform();
const arch = os.arch();
let releaseArch = 'amd64';
if (arch === 'arm64') releaseArch = 'arm64';
let releaseOs = 'linux';
let ext = 'tar.gz';
let binName = 'frpc';
if (platform === 'win32') {
releaseOs = 'windows';
ext = 'zip';
binName = 'frpc.exe';
} else if (platform === 'darwin') {
releaseOs = 'darwin';
}
const version = '0.58.1';
const folderName = `frp_${version}_${releaseOs}_${releaseArch}`;
const fileName = `${folderName}.${ext}`;
const downloadUrl = `https://github.com/fatedier/frp/releases/download/v${version}/${fileName}`;
const archivePath = path.join(destDir, fileName);
fs.mkdirSync(destDir, { recursive: true });
// Check if already exists
const finalBinPath = path.join(destDir, binName);
if (fs.existsSync(finalBinPath)) {
if (progressCallback) progressCallback(100);
return finalBinPath;
}
await downloadFile(downloadUrl, archivePath, progressCallback);
if (ext === 'zip') {
const zip = new AdmZip(archivePath);
zip.extractAllTo(destDir, true);
// move the binary out of the extracted folder
fs.renameSync(path.join(destDir, folderName, binName), finalBinPath);
fs.rmSync(path.join(destDir, folderName), { recursive: true, force: true });
} else {
// For tar.gz on Linux, we can use spawn 'tar'
const { spawnSync } = await import('child_process');
spawnSync('tar', ['-xzf', archivePath, '-C', destDir]);
fs.renameSync(path.join(destDir, folderName, binName), finalBinPath);
fs.rmSync(path.join(destDir, folderName), { recursive: true, force: true });
// make it executable
fs.chmodSync(finalBinPath, 0o755);
}
fs.unlinkSync(archivePath);
return finalBinPath;
}
// Auto-download ViaVersion suite for cross-play
export async function downloadViaPlugins(serverPath, software, onLog) {
const pluginsDir = path.join(serverPath, software === 'Fabric' ? 'mods' : 'plugins');
fs.mkdirSync(pluginsDir, { recursive: true });
const loader = software === 'Fabric' ? 'fabric' : 'paper';
const projects = ['viaversion', 'viabackwards', 'viarewind', 'geyser', 'floodgate'];
for (const project of projects) {
try {
onLog(`[SYSTEM] Resolving latest ${project} for ${loader}...`);
const res = await fetch(`https://api.modrinth.com/v2/project/${project}/version`, { headers: { 'User-Agent': 'techedstudios/mc-hoster' } });
if (!res.ok) continue;
const versions = await res.json();
if (!versions || versions.length === 0) continue;
let best = versions.find(v => v.loaders && v.loaders.includes(loader)) || versions[0];
const file = best.files.find(f => f.primary) || best.files[0];
if (!file) continue;
const destPath = path.join(pluginsDir, file.filename);
// Remove old version if exists
const existing = fs.readdirSync(pluginsDir).find(f => f.toLowerCase().startsWith(project.replace('-', '')) || f.toLowerCase().startsWith(project));
if (existing && existing !== file.filename) {
fs.unlinkSync(path.join(pluginsDir, existing));
}
if (fs.existsSync(destPath)) {
onLog(`[SYSTEM] ${file.filename} already installed.`);
continue;
}
onLog(`[SYSTEM] Downloading ${file.filename}...`);
await downloadFile(file.url, destPath, null);
onLog(`[SYSTEM] Installed ${project}.`);
} catch (e) {
onLog(`[SYSTEM] Failed to download ${project}: ${e.message}`);
}
}
}