Feat: Adding backup button for easy access to download a full server backup
This commit is contained in:
@ -201,3 +201,26 @@ export async function updateMods(docker, containerName) {
|
|||||||
return { error: `Failed to update mods: ${error.message}` };
|
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}` };
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { URLSearchParams } from 'url';
|
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 { checkConnectionStatus, checkGeyserStatus, checkSftpStatus } from './status.js';
|
||||||
import { apiRequest } from './api.js';
|
import { apiRequest } from './api.js';
|
||||||
|
|
||||||
@ -42,7 +42,6 @@ function startDockerStatsInterval(ws, client, user, docker) {
|
|||||||
const container = docker.getContainer(user);
|
const container = docker.getContainer(user);
|
||||||
const inspect = await container.inspect();
|
const inspect = await container.inspect();
|
||||||
if (inspect.State.Status !== 'running') {
|
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` }));
|
ws.send(JSON.stringify({ type: 'docker', error: `Container ${user} is not running` }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -247,7 +246,7 @@ async function manageStatusChecks(ws, client, user, docker) {
|
|||||||
if (data && data.hostname && data.port) {
|
if (data && data.hostname && data.port) {
|
||||||
const status = await checkFn(data.hostname, data.port);
|
const status = await checkFn(data.hostname, data.port);
|
||||||
if (ws.readyState === ws.OPEN) {
|
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) {
|
} 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);
|
response = client.user === 'Unknown' ? { error: 'User not identified' } : await writeServerProperties(docker, client.user, body.content);
|
||||||
} else if (endpoint === '/update-mods' && method === 'POST') {
|
} else if (endpoint === '/update-mods' && method === 'POST') {
|
||||||
response = client.user === 'Unknown' ? { error: 'User not identified' } : await updateMods(docker, client.user);
|
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 {
|
} else {
|
||||||
response = await apiRequest(endpoint, client.apiKey, method, body);
|
response = await apiRequest(endpoint, client.apiKey, method, body);
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
restartBtn: document.getElementById('restartBtn'),
|
restartBtn: document.getElementById('restartBtn'),
|
||||||
connectionStatus: document.getElementById('connectionStatus'),
|
connectionStatus: document.getElementById('connectionStatus'),
|
||||||
geyserStatus: document.getElementById('geyserStatus'),
|
geyserStatus: document.getElementById('geyserStatus'),
|
||||||
sftpStatus: document.getElementById('sftpStatus')
|
sftpStatus: document.getElementById('sftpStatus'),
|
||||||
|
backupBtn: document.getElementById('backupBtn')
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadouts = {
|
const loadouts = {
|
||||||
@ -298,7 +299,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
showMainContent();
|
showMainContent();
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
type: 'subscribe',
|
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(() => {
|
responseTimeout = setTimeout(() => {
|
||||||
showNotification('No response from server. Please check connection or API key.', 'error');
|
showNotification('No response from server. Please check connection or API key.', 'error');
|
||||||
@ -377,6 +392,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
updateSftpStatusUI(message);
|
updateSftpStatusUI(message);
|
||||||
} else if (message.type === 'update-mods') {
|
} else if (message.type === 'update-mods') {
|
||||||
updateModsUI(message);
|
updateModsUI(message);
|
||||||
|
} else if (message.type === 'backup') {
|
||||||
|
// Backup messages are primarily handled via wsRequest responses
|
||||||
|
console.log('Received backup message:', message);
|
||||||
} else {
|
} else {
|
||||||
updateNonDockerUI(message);
|
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 serverStatusSection = document.querySelector('.bg-gray-800.p-6.rounded-lg.shadow-lg.mb-6[data-section="server-status"]');
|
||||||
const editPropertiesBtn = elements.editPropertiesBtn;
|
const editPropertiesBtn = elements.editPropertiesBtn;
|
||||||
const updateModsBtn = elements.updateModsBtn;
|
const updateModsBtn = elements.updateModsBtn;
|
||||||
|
const backupBtn = elements.backupBtn;
|
||||||
const startBtn = document.getElementById('startBtn');
|
const startBtn = document.getElementById('startBtn');
|
||||||
const stopBtn = elements.stopBtn;
|
const stopBtn = elements.stopBtn;
|
||||||
const restartBtn = elements.restartBtn;
|
const restartBtn = elements.restartBtn;
|
||||||
@ -445,6 +464,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (updateModsBtn) {
|
if (updateModsBtn) {
|
||||||
updateModsBtn.classList.add('hidden');
|
updateModsBtn.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
if (backupBtn) {
|
||||||
|
backupBtn.classList.add('hidden');
|
||||||
|
}
|
||||||
if (stopBtn) {
|
if (stopBtn) {
|
||||||
stopBtn.disabled = true;
|
stopBtn.disabled = true;
|
||||||
stopBtn.classList.add('disabled-btn');
|
stopBtn.classList.add('disabled-btn');
|
||||||
@ -467,6 +489,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (updateModsBtn) {
|
if (updateModsBtn) {
|
||||||
updateModsBtn.classList.remove('hidden');
|
updateModsBtn.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
if (backupBtn) {
|
||||||
|
backupBtn.classList.remove('hidden');
|
||||||
|
}
|
||||||
if (stopBtn) {
|
if (stopBtn) {
|
||||||
stopBtn.disabled = false;
|
stopBtn.disabled = false;
|
||||||
stopBtn.classList.remove('disabled-btn');
|
stopBtn.classList.remove('disabled-btn');
|
||||||
@ -1226,7 +1251,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const content = propertiesToString(fullProperties);
|
const content = propertiesToString(fullProperties);
|
||||||
const response = await wsRequest('/server-properties', 'POST', { content });
|
const response = await wsRequest('/server-properties', 'POST', { content });
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
updateNotification(notification, `Failed to save server.properties: ${response.error}`, 'error');
|
showNotification(`Failed to save server.properties: ${response.error}`, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
elements.editPropertiesModal.classList.add('hidden');
|
elements.editPropertiesModal.classList.add('hidden');
|
||||||
@ -1239,15 +1264,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
async function updateMods() {
|
async function updateMods() {
|
||||||
try {
|
try {
|
||||||
const response = await wsRequest('/update-mods', 'POST');
|
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) {
|
if (response.error) {
|
||||||
showNotification(`Failed to update mods: ${response.error}`, 'error');
|
showNotification(`Failed to update mods: ${response.error}`, 'error');
|
||||||
elements.updateModsOutput.textContent = `Error: ${response.error}`;
|
elements.updateModsOutput.textContent = `Error: ${response.error}`;
|
||||||
} else {
|
} else {
|
||||||
const output = response.output || 'No output from mod update.';
|
const output = response.output || 'No output from mod update.';
|
||||||
console.log('Processed output:', output); // Debug: Log the final output
|
elements.updateModsOutput.textContent = output;
|
||||||
elements.updateModsOutput.textContent = output; // Set textContent
|
|
||||||
}
|
}
|
||||||
elements.updateModsModal.classList.remove('hidden');
|
elements.updateModsModal.classList.remove('hidden');
|
||||||
} catch (error) {
|
} 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', () => {
|
elements.loginBtn.addEventListener('click', () => {
|
||||||
apiKey = elements.loginApiKey.value.trim();
|
apiKey = elements.loginApiKey.value.trim();
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
@ -1314,21 +1362,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
initializeTerminal();
|
initializeTerminal();
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify({ type: 'subscribe', endpoints: ['docker', 'docker-logs'] }));
|
ws.send(JSON.stringify({ type: 'subscribe', endpoints: ['docker', 'docker-logs'] }));
|
||||||
// Set up a one-time message listener for the docker status
|
|
||||||
const messageHandler = (event) => {
|
const messageHandler = (event) => {
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(event.data);
|
const message = JSON.parse(event.data);
|
||||||
if (message.type === 'docker' && message.data?.status === 'running') {
|
if (message.type === 'docker' && message.data?.status === 'running') {
|
||||||
updateNotification(notification, 'Server started successfully', 'success');
|
updateNotification(notification, 'Server started successfully', 'success');
|
||||||
toggleSections('running');
|
toggleSections('running');
|
||||||
ws.removeEventListener('message', messageHandler); // Remove listener after success
|
ws.removeEventListener('message', messageHandler);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error parsing WebSocket message:', error);
|
console.error('Error parsing WebSocket message:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ws.addEventListener('message', messageHandler);
|
ws.addEventListener('message', messageHandler);
|
||||||
// Timeout to handle case where running status isn't received
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
ws.removeEventListener('message', messageHandler);
|
ws.removeEventListener('message', messageHandler);
|
||||||
@ -1336,7 +1382,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
updateNotification(notification, 'Server failed to start', 'error');
|
updateNotification(notification, 'Server failed to start', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 30000); // 30 seconds timeout
|
}, 30000);
|
||||||
} else {
|
} else {
|
||||||
updateNotification(notification, 'WebSocket not connected', 'error');
|
updateNotification(notification, 'WebSocket not connected', 'error');
|
||||||
}
|
}
|
||||||
@ -1378,6 +1424,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
elements.updateModsOutput.textContent = '';
|
elements.updateModsOutput.textContent = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
elements.backupBtn.addEventListener('click', createBackup);
|
||||||
|
|
||||||
document.getElementById('searchBtn').addEventListener('click', () => {
|
document.getElementById('searchBtn').addEventListener('click', () => {
|
||||||
searchMods(1);
|
searchMods(1);
|
||||||
});
|
});
|
||||||
|
@ -104,6 +104,7 @@
|
|||||||
<h1 class="text-2xl font-bold">My-MC Server Panel</h1>
|
<h1 class="text-2xl font-bold">My-MC Server Panel</h1>
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<button id="refresh" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded">Refresh</button>
|
<button id="refresh" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded">Refresh</button>
|
||||||
|
<button id="backupBtn" class="bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded">Backup</button>
|
||||||
<div id="authControls">
|
<div id="authControls">
|
||||||
<input id="apiKey" type="text" placeholder="Enter API Key" class="bg-gray-700 px-4 py-2 rounded text-white">
|
<input id="apiKey" type="text" placeholder="Enter API Key" class="bg-gray-700 px-4 py-2 rounded text-white">
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user