458 lines
16 KiB
JavaScript
458 lines
16 KiB
JavaScript
// DB Restore Tool - Frontend JavaScript
|
|
|
|
let currentPath = '';
|
|
let selectedFile = null;
|
|
let isConnected = false;
|
|
let currentJobId = null;
|
|
let pollInterval = null;
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadAvailableKeys();
|
|
setupEventListeners();
|
|
});
|
|
|
|
function setupEventListeners() {
|
|
// Auth type toggle
|
|
document.getElementById('auth-type').addEventListener('change', function() {
|
|
const passwordAuth = document.getElementById('password-auth');
|
|
const keyAuth = document.getElementById('key-auth');
|
|
if (this.value === 'password') {
|
|
passwordAuth.classList.remove('hidden');
|
|
keyAuth.classList.add('hidden');
|
|
} else {
|
|
passwordAuth.classList.add('hidden');
|
|
keyAuth.classList.remove('hidden');
|
|
}
|
|
});
|
|
|
|
// Connect form
|
|
document.getElementById('connect-form').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
connect();
|
|
});
|
|
|
|
// Disconnect button
|
|
document.getElementById('disconnect-btn').addEventListener('click', disconnect);
|
|
|
|
// Restore button
|
|
document.getElementById('restore-btn').addEventListener('click', startRestore);
|
|
|
|
// Cancel button
|
|
document.getElementById('cancel-btn').addEventListener('click', cancelRestore);
|
|
}
|
|
|
|
async function loadAvailableKeys() {
|
|
try {
|
|
const response = await fetch('/api/keys');
|
|
const data = await response.json();
|
|
const select = document.getElementById('key-file');
|
|
select.innerHTML = '<option value="">Select a key...</option>';
|
|
data.keys.forEach(key => {
|
|
const option = document.createElement('option');
|
|
option.value = key;
|
|
option.textContent = key;
|
|
select.appendChild(option);
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to load keys:', error);
|
|
}
|
|
}
|
|
|
|
async function connect() {
|
|
const authType = document.getElementById('auth-type').value;
|
|
const password = document.getElementById('password').value;
|
|
const keyFile = document.getElementById('key-file').value;
|
|
const keyPassphrase = document.getElementById('key-passphrase').value;
|
|
|
|
const btn = document.getElementById('connect-btn');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Connecting...';
|
|
|
|
try {
|
|
const response = await fetch('/api/connect', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
auth_type: authType,
|
|
password: password,
|
|
key_file: keyFile,
|
|
key_passphrase: keyPassphrase
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
isConnected = true;
|
|
currentPath = data.path;
|
|
showStatus('Connected to ' + data.host, 'success');
|
|
renderFiles(data.files, data.path);
|
|
updateBreadcrumb(data.path);
|
|
|
|
// Toggle buttons
|
|
btn.classList.add('hidden');
|
|
document.getElementById('disconnect-btn').classList.remove('hidden');
|
|
|
|
// Clear password field for security
|
|
document.getElementById('password').value = '';
|
|
} else {
|
|
showStatus('Connection failed: ' + data.error, 'error');
|
|
}
|
|
} catch (error) {
|
|
showStatus('Connection error: ' + error.message, 'error');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.textContent = 'Connect';
|
|
}
|
|
}
|
|
|
|
async function disconnect() {
|
|
try {
|
|
await fetch('/api/disconnect', { method: 'POST' });
|
|
} catch (e) {}
|
|
|
|
isConnected = false;
|
|
selectedFile = null;
|
|
currentPath = '';
|
|
|
|
// Reset UI
|
|
document.getElementById('connect-btn').classList.remove('hidden');
|
|
document.getElementById('disconnect-btn').classList.add('hidden');
|
|
document.getElementById('file-browser').innerHTML = `
|
|
<div class="p-8 text-center text-gray-400">
|
|
<svg class="w-16 h-16 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 14v3m4-3v3m4-3v3M3 21h18M3 10h18M3 7l9-4 9 4M4 10h16v11H4V10z"/>
|
|
</svg>
|
|
<p>Connect to browse remote files</p>
|
|
</div>
|
|
`;
|
|
document.getElementById('breadcrumb').innerHTML = '<span class="text-gray-400">Not connected</span>';
|
|
document.getElementById('selected-file-info').innerHTML = '<p class="text-gray-500 text-sm">No file selected</p>';
|
|
document.getElementById('restore-btn').disabled = true;
|
|
|
|
hideStatus();
|
|
}
|
|
|
|
async function browse(path) {
|
|
if (!isConnected) return;
|
|
|
|
try {
|
|
const response = await fetch('/api/browse', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ path: path })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
currentPath = data.path;
|
|
renderFiles(data.files, data.path);
|
|
updateBreadcrumb(data.path);
|
|
} else {
|
|
showStatus('Browse failed: ' + data.error, 'error');
|
|
}
|
|
} catch (error) {
|
|
showStatus('Browse error: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
function renderFiles(files, path) {
|
|
const container = document.getElementById('file-browser');
|
|
|
|
if (files.length === 0) {
|
|
container.innerHTML = '<div class="p-8 text-center text-gray-400">Empty directory</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="divide-y">';
|
|
|
|
// Parent directory link
|
|
if (path !== '/') {
|
|
const parentPath = path.split('/').slice(0, -1).join('/') || '/';
|
|
html += `
|
|
<div class="file-item p-3 cursor-pointer flex items-center" onclick="browse('${parentPath}')">
|
|
<svg class="w-5 h-5 mr-3 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 17l-5-5m0 0l5-5m-5 5h12"/>
|
|
</svg>
|
|
<span class="text-gray-600">..</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
files.forEach(file => {
|
|
const isSelected = selectedFile && selectedFile.path === file.path;
|
|
const selectedClass = isSelected ? 'selected' : '';
|
|
|
|
if (file.is_dir) {
|
|
html += `
|
|
<div class="file-item p-3 cursor-pointer flex items-center ${selectedClass}" onclick="browse('${file.path}')">
|
|
<svg class="w-5 h-5 mr-3 text-yellow-500" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M10 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z"/>
|
|
</svg>
|
|
<span class="flex-1 font-medium">${file.name}</span>
|
|
</div>
|
|
`;
|
|
} else if (file.is_sql) {
|
|
html += `
|
|
<div class="file-item p-3 cursor-pointer flex items-center ${selectedClass}" onclick="selectFile(${JSON.stringify(file).replace(/"/g, '"')})">
|
|
<svg class="w-5 h-5 mr-3 text-green-500" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z"/>
|
|
<path d="M14 2v6h6"/>
|
|
</svg>
|
|
<span class="flex-1">${file.name}</span>
|
|
<span class="text-sm text-gray-500 mr-4">${file.size_human}</span>
|
|
<span class="text-sm text-gray-400">${file.mtime_human}</span>
|
|
</div>
|
|
`;
|
|
} else {
|
|
html += `
|
|
<div class="file-item p-3 flex items-center opacity-50">
|
|
<svg class="w-5 h-5 mr-3 text-gray-400" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z"/>
|
|
</svg>
|
|
<span class="flex-1 text-gray-500">${file.name}</span>
|
|
<span class="text-sm text-gray-400">${file.size_human}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function selectFile(file) {
|
|
selectedFile = file;
|
|
document.getElementById('selected-file-info').innerHTML = `
|
|
<p class="font-medium text-gray-800">${file.name}</p>
|
|
<p class="text-sm text-gray-500">${file.size_human} - ${file.mtime_human}</p>
|
|
`;
|
|
document.getElementById('restore-btn').disabled = false;
|
|
|
|
// Auto-detect target database from filename
|
|
const filename = file.name.toLowerCase();
|
|
const targetDbSelect = document.getElementById('target-db');
|
|
const availableDbs = Array.from(targetDbSelect.options).map(o => o.value);
|
|
|
|
for (const db of availableDbs) {
|
|
if (filename.includes(db.toLowerCase())) {
|
|
targetDbSelect.value = db;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Re-render to show selection
|
|
browse(currentPath);
|
|
}
|
|
|
|
function updateBreadcrumb(path) {
|
|
const parts = path.split('/').filter(p => p);
|
|
let html = `<span class="cursor-pointer hover:text-blue-600" onclick="browse('/')">/</span>`;
|
|
|
|
let currentPathBuild = '';
|
|
parts.forEach((part, index) => {
|
|
currentPathBuild += '/' + part;
|
|
const isLast = index === parts.length - 1;
|
|
html += `
|
|
<span class="mx-1">/</span>
|
|
<span class="${isLast ? 'font-medium' : 'cursor-pointer hover:text-blue-600'}"
|
|
${isLast ? '' : `onclick="browse('${currentPathBuild}')"`}>${part}</span>
|
|
`;
|
|
});
|
|
|
|
document.getElementById('breadcrumb').innerHTML = html;
|
|
}
|
|
|
|
async function startRestore() {
|
|
if (!selectedFile) return;
|
|
|
|
const targetDb = document.getElementById('target-db').value;
|
|
|
|
if (!confirm(`Are you sure you want to restore ${selectedFile.name} to database "${targetDb}"?\n\nThis will DROP ALL TABLES in the database!`)) {
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById('restore-btn');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Starting...';
|
|
|
|
try {
|
|
const response = await fetch('/api/restore', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
file: selectedFile.path,
|
|
database: targetDb
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
currentJobId = data.job_id;
|
|
showProgressPanel();
|
|
startPolling();
|
|
} else {
|
|
showStatus('Restore failed: ' + data.error, 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = 'Start Restore';
|
|
}
|
|
} catch (error) {
|
|
showStatus('Restore error: ' + error.message, 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = 'Start Restore';
|
|
}
|
|
}
|
|
|
|
function showProgressPanel() {
|
|
document.getElementById('progress-panel').classList.remove('hidden');
|
|
document.getElementById('progress-bar').style.width = '0%';
|
|
document.getElementById('progress-bar').classList.remove('bg-green-600', 'bg-red-600', 'bg-yellow-600');
|
|
document.getElementById('progress-bar').classList.add('bg-blue-600');
|
|
document.getElementById('progress-percent').textContent = '0%';
|
|
document.getElementById('progress-status').textContent = 'Starting...';
|
|
document.getElementById('progress-message').textContent = 'Initializing...';
|
|
document.getElementById('cancel-btn').classList.remove('hidden');
|
|
document.getElementById('cancel-btn').disabled = false;
|
|
document.getElementById('cancel-btn').textContent = 'Cancel Restore';
|
|
}
|
|
|
|
function startPolling() {
|
|
if (pollInterval) clearInterval(pollInterval);
|
|
|
|
pollInterval = setInterval(async () => {
|
|
try {
|
|
const response = await fetch(`/api/status/${currentJobId}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
updateProgress(data);
|
|
|
|
if (data.status === 'completed' || data.status === 'error' || data.status === 'cancelled') {
|
|
stopPolling();
|
|
document.getElementById('restore-btn').disabled = false;
|
|
document.getElementById('restore-btn').textContent = 'Start Restore';
|
|
|
|
if (data.status === 'completed') {
|
|
showStatus('Restore completed successfully!', 'success');
|
|
} else if (data.status === 'cancelled') {
|
|
showStatus('Restore was cancelled', 'error');
|
|
} else {
|
|
showStatus('Restore failed: ' + data.error, 'error');
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Polling error:', error);
|
|
}
|
|
}, 250);
|
|
}
|
|
|
|
function stopPolling() {
|
|
if (pollInterval) {
|
|
clearInterval(pollInterval);
|
|
pollInterval = null;
|
|
}
|
|
}
|
|
|
|
async function cancelRestore() {
|
|
if (!currentJobId) return;
|
|
|
|
if (!confirm('Are you sure you want to cancel the restore?')) {
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById('cancel-btn');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Cancelling...';
|
|
|
|
try {
|
|
const response = await fetch(`/api/cancel/${currentJobId}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showStatus('Cancellation requested...', 'error');
|
|
} else {
|
|
showStatus('Cancel failed: ' + data.error, 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = 'Cancel Restore';
|
|
}
|
|
} catch (error) {
|
|
showStatus('Cancel error: ' + error.message, 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = 'Cancel Restore';
|
|
}
|
|
}
|
|
|
|
function updateProgress(data) {
|
|
const bar = document.getElementById('progress-bar');
|
|
const percent = document.getElementById('progress-percent');
|
|
const status = document.getElementById('progress-status');
|
|
const message = document.getElementById('progress-message');
|
|
const elapsed = document.getElementById('progress-elapsed');
|
|
const cancelBtn = document.getElementById('cancel-btn');
|
|
|
|
bar.style.width = data.progress + '%';
|
|
percent.textContent = data.progress + '%';
|
|
|
|
// Update status label
|
|
const statusLabels = {
|
|
'starting': 'Starting',
|
|
'downloading': 'Downloading',
|
|
'restoring': 'Restoring',
|
|
'completed': 'Completed',
|
|
'error': 'Error',
|
|
'cancelled': 'Cancelled'
|
|
};
|
|
status.textContent = statusLabels[data.status] || data.status;
|
|
|
|
// Update color based on status
|
|
bar.classList.remove('bg-green-600', 'bg-red-600', 'bg-yellow-600');
|
|
if (data.status === 'completed') {
|
|
bar.classList.remove('bg-blue-600');
|
|
bar.classList.add('bg-green-600');
|
|
} else if (data.status === 'error') {
|
|
bar.classList.remove('bg-blue-600');
|
|
bar.classList.add('bg-red-600');
|
|
} else if (data.status === 'cancelled') {
|
|
bar.classList.remove('bg-blue-600');
|
|
bar.classList.add('bg-yellow-600');
|
|
}
|
|
|
|
// Show/hide cancel button based on job status
|
|
if (data.status === 'completed' || data.status === 'error' || data.status === 'cancelled') {
|
|
cancelBtn.classList.add('hidden');
|
|
} else {
|
|
cancelBtn.classList.remove('hidden');
|
|
cancelBtn.disabled = false;
|
|
cancelBtn.textContent = 'Cancel Restore';
|
|
}
|
|
|
|
message.textContent = data.message || '';
|
|
if (data.elapsed) {
|
|
elapsed.textContent = 'Elapsed: ' + data.elapsed;
|
|
}
|
|
}
|
|
|
|
function showStatus(message, type) {
|
|
const status = document.getElementById('connection-status');
|
|
status.classList.remove('hidden', 'bg-green-100', 'bg-red-100', 'text-green-800', 'text-red-800');
|
|
|
|
if (type === 'success') {
|
|
status.classList.add('bg-green-100', 'text-green-800');
|
|
} else if (type === 'error') {
|
|
status.classList.add('bg-red-100', 'text-red-800');
|
|
}
|
|
|
|
status.textContent = message;
|
|
}
|
|
|
|
function hideStatus() {
|
|
document.getElementById('connection-status').classList.add('hidden');
|
|
}
|