diff --git a/includes/docker.js b/includes/docker.js index 403d1bc..d7970fe 100644 --- a/includes/docker.js +++ b/includes/docker.js @@ -200,4 +200,27 @@ export async function updateMods(docker, containerName) { } catch (error) { return { error: `Failed to update mods: ${error.message}` }; } +} + +export async function createBackup(docker, containerName) { + try { + const container = docker.getContainer(containerName); + const inspect = await container.inspect(); + if (inspect.State.Status !== 'running') { + return { error: `Container ${containerName} is not running` }; + } + const command = `docker exec -t ${containerName} bash -c "/home/backup.sh | grep export"`; + const { stdout, stderr } = await execPromise(command); + if (stderr) return { error: stderr }; + + // Extract the URL using a regular expression + const urlRegex = /(https:\/\/[^\s]+)/; + const match = stdout.match(urlRegex); + if (!match) return { error: 'No download URL found in backup output' }; + + const downloadURL = match[0]; + return { output: 'Backup completed successfully', downloadURL }; + } catch (error) { + return { error: `Failed to create backup: ${error.message}` }; + } } \ No newline at end of file diff --git a/includes/websocket.js b/includes/websocket.js index 8a01fc9..a714b9d 100644 --- a/includes/websocket.js +++ b/includes/websocket.js @@ -1,5 +1,5 @@ import { URLSearchParams } from 'url'; -import { getContainerStats, streamContainerLogs, readServerProperties, writeServerProperties, updateMods } from './docker.js'; +import { getContainerStats, streamContainerLogs, readServerProperties, writeServerProperties, updateMods, createBackup } from './docker.js'; import { checkConnectionStatus, checkGeyserStatus, checkSftpStatus } from './status.js'; import { apiRequest } from './api.js'; @@ -42,7 +42,6 @@ function startDockerStatsInterval(ws, client, user, docker) { const container = docker.getContainer(user); const inspect = await container.inspect(); if (inspect.State.Status !== 'running') { - // console.log(`Container ${user} not running, sending error`); ws.send(JSON.stringify({ type: 'docker', error: `Container ${user} is not running` })); return; } @@ -247,7 +246,7 @@ async function manageStatusChecks(ws, client, user, docker) { if (data && data.hostname && data.port) { const status = await checkFn(data.hostname, data.port); if (ws.readyState === ws.OPEN) { - ws.send(JSON.stringify({ type: 'connection-status', data: { isOnline: status.isOnline } })); + ws.send(JSON.stringify({ type: statusType, data: { isOnline: status.isOnline } })); } } } catch (error) { @@ -483,6 +482,8 @@ export function handleWebSocket(ws, req, docker) { response = client.user === 'Unknown' ? { error: 'User not identified' } : await writeServerProperties(docker, client.user, body.content); } else if (endpoint === '/update-mods' && method === 'POST') { response = client.user === 'Unknown' ? { error: 'User not identified' } : await updateMods(docker, client.user); + } else if (endpoint === '/backup' && method === 'POST') { + response = client.user === 'Unknown' ? { error: 'User not identified' } : await createBackup(docker, client.user); } else { response = await apiRequest(endpoint, client.apiKey, method, body); } diff --git a/public/app.js b/public/app.js index 5547d5b..98efdb9 100644 --- a/public/app.js +++ b/public/app.js @@ -78,7 +78,8 @@ document.addEventListener('DOMContentLoaded', () => { restartBtn: document.getElementById('restartBtn'), connectionStatus: document.getElementById('connectionStatus'), geyserStatus: document.getElementById('geyserStatus'), - sftpStatus: document.getElementById('sftpStatus') + sftpStatus: document.getElementById('sftpStatus'), + backupBtn: document.getElementById('backupBtn') }; const loadouts = { @@ -298,7 +299,21 @@ document.addEventListener('DOMContentLoaded', () => { showMainContent(); ws.send(JSON.stringify({ type: 'subscribe', - endpoints: ['docker', 'docker-logs', 'hello', 'time', 'list-players', 'mod-list', 'log', 'website', 'map', 'my-link-cache', 'my-geyser-cache', 'my-sftp-cache'] + endpoints: [ + 'docker', + 'docker-logs', + 'hello', + 'time', + 'list-players', + 'mod-list', + 'log', + 'website', + 'map', + 'my-link-cache', + 'my-geyser-cache', + 'my-sftp-cache', + 'backup' + ] })); responseTimeout = setTimeout(() => { showNotification('No response from server. Please check connection or API key.', 'error'); @@ -377,6 +392,9 @@ document.addEventListener('DOMContentLoaded', () => { updateSftpStatusUI(message); } else if (message.type === 'update-mods') { updateModsUI(message); + } else if (message.type === 'backup') { + // Backup messages are primarily handled via wsRequest responses + console.log('Received backup message:', message); } else { updateNonDockerUI(message); } @@ -419,6 +437,7 @@ document.addEventListener('DOMContentLoaded', () => { const serverStatusSection = document.querySelector('.bg-gray-800.p-6.rounded-lg.shadow-lg.mb-6[data-section="server-status"]'); const editPropertiesBtn = elements.editPropertiesBtn; const updateModsBtn = elements.updateModsBtn; + const backupBtn = elements.backupBtn; const startBtn = document.getElementById('startBtn'); const stopBtn = elements.stopBtn; const restartBtn = elements.restartBtn; @@ -445,6 +464,9 @@ document.addEventListener('DOMContentLoaded', () => { if (updateModsBtn) { updateModsBtn.classList.add('hidden'); } + if (backupBtn) { + backupBtn.classList.add('hidden'); + } if (stopBtn) { stopBtn.disabled = true; stopBtn.classList.add('disabled-btn'); @@ -467,6 +489,9 @@ document.addEventListener('DOMContentLoaded', () => { if (updateModsBtn) { updateModsBtn.classList.remove('hidden'); } + if (backupBtn) { + backupBtn.classList.remove('hidden'); + } if (stopBtn) { stopBtn.disabled = false; stopBtn.classList.remove('disabled-btn'); @@ -1226,7 +1251,7 @@ document.addEventListener('DOMContentLoaded', () => { const content = propertiesToString(fullProperties); const response = await wsRequest('/server-properties', 'POST', { content }); if (response.error) { - updateNotification(notification, `Failed to save server.properties: ${response.error}`, 'error'); + showNotification(`Failed to save server.properties: ${response.error}`, 'error'); return; } elements.editPropertiesModal.classList.add('hidden'); @@ -1239,15 +1264,12 @@ document.addEventListener('DOMContentLoaded', () => { async function updateMods() { try { const response = await wsRequest('/update-mods', 'POST'); - console.log('Raw response:', response); // Debug: Log the full response - console.log('Output content:', response.output); // Debug: Log the output string if (response.error) { showNotification(`Failed to update mods: ${response.error}`, 'error'); elements.updateModsOutput.textContent = `Error: ${response.error}`; } else { const output = response.output || 'No output from mod update.'; - console.log('Processed output:', output); // Debug: Log the final output - elements.updateModsOutput.textContent = output; // Set textContent + elements.updateModsOutput.textContent = output; } elements.updateModsModal.classList.remove('hidden'); } catch (error) { @@ -1258,6 +1280,32 @@ document.addEventListener('DOMContentLoaded', () => { } } + async function createBackup() { + try { + showNotification('Your backup is being created, a download will begin once ready!', 'success'); + const response = await wsRequest('/backup', 'POST'); + if (response.error) { + showNotification(`Failed to create backup: ${response.error}`, 'error'); + return; + } + const downloadURL = response.downloadURL; + if (downloadURL) { + const link = document.createElement('a'); + link.href = downloadURL; + link.download = ''; // Let the browser infer the filename from the URL + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + showNotification('Backup download initiated', 'success'); + } else { + showNotification('Backup created but no download URL provided', 'error'); + } + } catch (error) { + console.error('Create backup error:', error); + showNotification(`Failed to create backup: ${error.message}`, 'error'); + } + } + elements.loginBtn.addEventListener('click', () => { apiKey = elements.loginApiKey.value.trim(); if (apiKey) { @@ -1314,21 +1362,19 @@ document.addEventListener('DOMContentLoaded', () => { initializeTerminal(); if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'subscribe', endpoints: ['docker', 'docker-logs'] })); - // Set up a one-time message listener for the docker status const messageHandler = (event) => { try { const message = JSON.parse(event.data); if (message.type === 'docker' && message.data?.status === 'running') { updateNotification(notification, 'Server started successfully', 'success'); toggleSections('running'); - ws.removeEventListener('message', messageHandler); // Remove listener after success + ws.removeEventListener('message', messageHandler); } } catch (error) { console.error('Error parsing WebSocket message:', error); } }; ws.addEventListener('message', messageHandler); - // Timeout to handle case where running status isn't received setTimeout(() => { if (ws && ws.readyState === WebSocket.OPEN) { ws.removeEventListener('message', messageHandler); @@ -1336,7 +1382,7 @@ document.addEventListener('DOMContentLoaded', () => { updateNotification(notification, 'Server failed to start', 'error'); } } - }, 30000); // 30 seconds timeout + }, 30000); } else { updateNotification(notification, 'WebSocket not connected', 'error'); } @@ -1378,6 +1424,8 @@ document.addEventListener('DOMContentLoaded', () => { elements.updateModsOutput.textContent = ''; }); + elements.backupBtn.addEventListener('click', createBackup); + document.getElementById('searchBtn').addEventListener('click', () => { searchMods(1); }); diff --git a/public/index.html b/public/index.html index 09ee036..29c6a73 100644 --- a/public/index.html +++ b/public/index.html @@ -104,6 +104,7 @@

My-MC Server Panel

+