mirror of
https://github.com/plexguide/Huntarr.io.git
synced 2025-12-17 20:24:22 -06:00
606 lines
22 KiB
JavaScript
606 lines
22 KiB
JavaScript
/**
|
||
* Backup and Restore functionality for Huntarr
|
||
* Handles database backup creation, restoration, and management
|
||
*/
|
||
|
||
const BackupRestore = {
|
||
initialized: false,
|
||
backupSettings: {
|
||
frequency: 3,
|
||
retention: 3
|
||
},
|
||
|
||
initialize: function() {
|
||
if (this.initialized) {
|
||
console.log('[BackupRestore] Already initialized');
|
||
return;
|
||
}
|
||
|
||
console.log('[BackupRestore] Initializing backup/restore functionality');
|
||
|
||
this.bindEvents();
|
||
this.loadSettings();
|
||
this.loadBackupList();
|
||
this.updateNextBackupTime();
|
||
|
||
this.initialized = true;
|
||
console.log('[BackupRestore] Initialization complete');
|
||
},
|
||
|
||
bindEvents: function() {
|
||
// Backup frequency change
|
||
const frequencyInput = document.getElementById('backup-frequency');
|
||
if (frequencyInput) {
|
||
frequencyInput.addEventListener('change', () => {
|
||
this.backupSettings.frequency = parseInt(frequencyInput.value) || 3;
|
||
this.saveSettings();
|
||
this.updateNextBackupTime();
|
||
});
|
||
}
|
||
|
||
// Backup retention change
|
||
const retentionInput = document.getElementById('backup-retention');
|
||
if (retentionInput) {
|
||
retentionInput.addEventListener('change', () => {
|
||
this.backupSettings.retention = parseInt(retentionInput.value) || 3;
|
||
this.saveSettings();
|
||
});
|
||
}
|
||
|
||
// Create manual backup
|
||
const createBackupBtn = document.getElementById('create-backup-btn');
|
||
if (createBackupBtn) {
|
||
createBackupBtn.addEventListener('click', () => {
|
||
this.createManualBackup();
|
||
});
|
||
}
|
||
|
||
// Restore backup selection
|
||
const restoreSelect = document.getElementById('restore-backup-select');
|
||
if (restoreSelect) {
|
||
restoreSelect.addEventListener('change', () => {
|
||
this.handleRestoreSelection();
|
||
});
|
||
}
|
||
|
||
// Restore confirmation input
|
||
const restoreConfirmation = document.getElementById('restore-confirmation');
|
||
if (restoreConfirmation) {
|
||
restoreConfirmation.addEventListener('input', () => {
|
||
this.validateRestoreConfirmation();
|
||
});
|
||
}
|
||
|
||
// Restore button
|
||
const restoreBtn = document.getElementById('restore-backup-btn');
|
||
if (restoreBtn) {
|
||
restoreBtn.addEventListener('click', () => {
|
||
this.restoreBackup();
|
||
});
|
||
}
|
||
|
||
// Delete confirmation input
|
||
const deleteConfirmation = document.getElementById('delete-confirmation');
|
||
if (deleteConfirmation) {
|
||
deleteConfirmation.addEventListener('input', () => {
|
||
this.validateDeleteConfirmation();
|
||
});
|
||
}
|
||
|
||
// Delete database button
|
||
const deleteBtn = document.getElementById('delete-database-btn');
|
||
if (deleteBtn) {
|
||
deleteBtn.addEventListener('click', () => {
|
||
this.deleteDatabase();
|
||
});
|
||
}
|
||
},
|
||
|
||
loadSettings: function() {
|
||
console.log('[BackupRestore] Loading backup settings');
|
||
|
||
fetch('./api/backup/settings')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
this.backupSettings = {
|
||
frequency: data.settings.frequency || 3,
|
||
retention: data.settings.retention || 3
|
||
};
|
||
|
||
// Update UI
|
||
const frequencyInput = document.getElementById('backup-frequency');
|
||
const retentionInput = document.getElementById('backup-retention');
|
||
|
||
if (frequencyInput) frequencyInput.value = this.backupSettings.frequency;
|
||
if (retentionInput) retentionInput.value = this.backupSettings.retention;
|
||
|
||
this.updateNextBackupTime();
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('[BackupRestore] Error loading settings:', error);
|
||
this.showError('Failed to load backup settings');
|
||
});
|
||
},
|
||
|
||
saveSettings: function() {
|
||
console.log('[BackupRestore] Saving backup settings', this.backupSettings);
|
||
|
||
fetch('./api/backup/settings', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(this.backupSettings)
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
console.log('[BackupRestore] Settings saved successfully');
|
||
this.showSuccess('Backup settings saved');
|
||
} else {
|
||
throw new Error(data.error || 'Failed to save settings');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('[BackupRestore] Error saving settings:', error);
|
||
this.showError('Failed to save backup settings');
|
||
});
|
||
},
|
||
|
||
loadBackupList: function() {
|
||
console.log('[BackupRestore] Loading backup list');
|
||
|
||
const listContainer = document.getElementById('backup-list-container');
|
||
const restoreSelect = document.getElementById('restore-backup-select');
|
||
|
||
if (listContainer) {
|
||
listContainer.innerHTML = '<div class="backup-list-loading"><i class="fas fa-spinner fa-spin"></i> Loading backup list...</div>';
|
||
}
|
||
|
||
if (restoreSelect) {
|
||
restoreSelect.innerHTML = '<option value="">Loading available backups...</option>';
|
||
}
|
||
|
||
fetch('./api/backup/list')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
this.renderBackupList(data.backups);
|
||
this.populateRestoreSelect(data.backups);
|
||
} else {
|
||
throw new Error(data.error || 'Failed to load backups');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('[BackupRestore] Error loading backup list:', error);
|
||
if (listContainer) {
|
||
listContainer.innerHTML = '<div class="backup-list-loading">Error loading backup list</div>';
|
||
}
|
||
if (restoreSelect) {
|
||
restoreSelect.innerHTML = '<option value="">Error loading backups</option>';
|
||
}
|
||
});
|
||
},
|
||
|
||
renderBackupList: function(backups) {
|
||
const listContainer = document.getElementById('backup-list-container');
|
||
if (!listContainer) return;
|
||
|
||
if (!backups || backups.length === 0) {
|
||
listContainer.innerHTML = '<div class="backup-list-loading">No backups available</div>';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
backups.forEach(backup => {
|
||
const date = new Date(backup.timestamp);
|
||
const formattedDate = date.toLocaleString();
|
||
const size = this.formatFileSize(backup.size);
|
||
|
||
html += `
|
||
<div class="backup-item" data-backup-id="${backup.id}">
|
||
<div class="backup-info">
|
||
<div class="backup-name">${backup.name}</div>
|
||
<div class="backup-details">
|
||
Created: ${formattedDate} | Size: ${size} | Type: ${backup.type || 'Manual'}
|
||
</div>
|
||
</div>
|
||
<div class="backup-actions">
|
||
<button class="delete-backup-btn" onclick="BackupRestore.deleteBackup('${backup.id}')">
|
||
<i class="fas fa-trash"></i> Delete
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
listContainer.innerHTML = html;
|
||
},
|
||
|
||
populateRestoreSelect: function(backups) {
|
||
const restoreSelect = document.getElementById('restore-backup-select');
|
||
if (!restoreSelect) return;
|
||
|
||
if (!backups || backups.length === 0) {
|
||
restoreSelect.innerHTML = '<option value="">No backups available</option>';
|
||
return;
|
||
}
|
||
|
||
let html = '<option value="">Select a backup to restore...</option>';
|
||
backups.forEach(backup => {
|
||
const date = new Date(backup.timestamp);
|
||
const formattedDate = date.toLocaleString();
|
||
const size = this.formatFileSize(backup.size);
|
||
|
||
html += `<option value="${backup.id}">${backup.name} - ${formattedDate} (${size})</option>`;
|
||
});
|
||
|
||
restoreSelect.innerHTML = html;
|
||
},
|
||
|
||
updateNextBackupTime: function() {
|
||
const nextBackupElement = document.getElementById('next-backup-time');
|
||
if (!nextBackupElement) return;
|
||
|
||
fetch('./api/backup/next-scheduled')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success && data.next_backup) {
|
||
const nextDate = new Date(data.next_backup);
|
||
nextBackupElement.innerHTML = `<i class="fas fa-clock"></i> ${nextDate.toLocaleString()}`;
|
||
} else {
|
||
nextBackupElement.innerHTML = '<i class="fas fa-clock"></i> Not scheduled';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('[BackupRestore] Error getting next backup time:', error);
|
||
nextBackupElement.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Error loading schedule';
|
||
});
|
||
},
|
||
|
||
createManualBackup: function() {
|
||
console.log('[BackupRestore] Creating manual backup');
|
||
|
||
const createBtn = document.getElementById('create-backup-btn');
|
||
const progressContainer = document.getElementById('backup-progress');
|
||
const progressFill = document.querySelector('.progress-fill');
|
||
const progressText = document.querySelector('.progress-text');
|
||
|
||
if (createBtn) createBtn.disabled = true;
|
||
if (progressContainer) progressContainer.style.display = 'block';
|
||
if (progressText) progressText.textContent = 'Creating backup...';
|
||
|
||
// Animate progress bar
|
||
let progress = 0;
|
||
const progressInterval = setInterval(() => {
|
||
progress += Math.random() * 15;
|
||
if (progress > 90) progress = 90;
|
||
if (progressFill) progressFill.style.width = progress + '%';
|
||
}, 200);
|
||
|
||
fetch('./api/backup/create', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
type: 'manual',
|
||
name: `Manual Backup ${new Date().toISOString().split('T')[0]}`
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
clearInterval(progressInterval);
|
||
|
||
if (data.success) {
|
||
if (progressFill) progressFill.style.width = '100%';
|
||
if (progressText) progressText.textContent = 'Backup created successfully!';
|
||
|
||
setTimeout(() => {
|
||
if (progressContainer) progressContainer.style.display = 'none';
|
||
if (progressFill) progressFill.style.width = '0%';
|
||
}, 2000);
|
||
|
||
this.showSuccess(`Backup created successfully: ${data.backup_name}`);
|
||
this.loadBackupList(); // Refresh the list
|
||
} else {
|
||
throw new Error(data.error || 'Failed to create backup');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
clearInterval(progressInterval);
|
||
console.error('[BackupRestore] Error creating backup:', error);
|
||
|
||
if (progressContainer) progressContainer.style.display = 'none';
|
||
if (progressFill) progressFill.style.width = '0%';
|
||
|
||
this.showError('Failed to create backup: ' + error.message);
|
||
})
|
||
.finally(() => {
|
||
if (createBtn) createBtn.disabled = false;
|
||
});
|
||
},
|
||
|
||
handleRestoreSelection: function() {
|
||
const restoreSelect = document.getElementById('restore-backup-select');
|
||
const confirmationGroup = document.getElementById('restore-confirmation-group');
|
||
const actionGroup = document.getElementById('restore-action-group');
|
||
|
||
if (!restoreSelect) return;
|
||
|
||
if (restoreSelect.value) {
|
||
if (confirmationGroup) confirmationGroup.style.display = 'block';
|
||
if (actionGroup) actionGroup.style.display = 'block';
|
||
} else {
|
||
if (confirmationGroup) confirmationGroup.style.display = 'none';
|
||
if (actionGroup) actionGroup.style.display = 'none';
|
||
}
|
||
|
||
this.validateRestoreConfirmation();
|
||
},
|
||
|
||
validateRestoreConfirmation: function() {
|
||
const confirmationInput = document.getElementById('restore-confirmation');
|
||
const restoreBtn = document.getElementById('restore-backup-btn');
|
||
|
||
if (!confirmationInput || !restoreBtn) return;
|
||
|
||
const isValid = confirmationInput.value.toUpperCase() === 'RESTORE';
|
||
restoreBtn.disabled = !isValid;
|
||
|
||
if (isValid) {
|
||
restoreBtn.style.background = '#e74c3c';
|
||
restoreBtn.style.cursor = 'pointer';
|
||
} else {
|
||
restoreBtn.style.background = '#6b7280';
|
||
restoreBtn.style.cursor = 'not-allowed';
|
||
}
|
||
},
|
||
|
||
restoreBackup: function() {
|
||
const restoreSelect = document.getElementById('restore-backup-select');
|
||
const confirmationInput = document.getElementById('restore-confirmation');
|
||
|
||
if (!restoreSelect || !confirmationInput) return;
|
||
|
||
const backupId = restoreSelect.value;
|
||
const confirmation = confirmationInput.value.toUpperCase();
|
||
|
||
if (!backupId || confirmation !== 'RESTORE') {
|
||
this.showError('Please select a backup and type RESTORE to confirm');
|
||
return;
|
||
}
|
||
|
||
// Final confirmation dialog
|
||
if (!confirm('This will permanently overwrite your current database. Are you absolutely sure?')) {
|
||
return;
|
||
}
|
||
|
||
console.log('[BackupRestore] Restoring backup:', backupId);
|
||
|
||
const restoreBtn = document.getElementById('restore-backup-btn');
|
||
if (restoreBtn) {
|
||
restoreBtn.disabled = true;
|
||
restoreBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Restoring...';
|
||
}
|
||
|
||
fetch('./api/backup/restore', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
backup_id: backupId
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
this.showSuccess('Database restored successfully! Reloading page...');
|
||
|
||
// Reload the page after a short delay
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 2000);
|
||
} else {
|
||
throw new Error(data.error || 'Failed to restore backup');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('[BackupRestore] Error restoring backup:', error);
|
||
this.showError('Failed to restore backup: ' + error.message);
|
||
})
|
||
.finally(() => {
|
||
if (restoreBtn) {
|
||
restoreBtn.disabled = false;
|
||
restoreBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Restore Database';
|
||
}
|
||
});
|
||
},
|
||
|
||
validateDeleteConfirmation: function() {
|
||
const confirmationInput = document.getElementById('delete-confirmation');
|
||
const actionGroup = document.getElementById('delete-action-group');
|
||
const deleteBtn = document.getElementById('delete-database-btn');
|
||
|
||
if (!confirmationInput || !actionGroup || !deleteBtn) return;
|
||
|
||
const isValid = confirmationInput.value.toLowerCase() === 'huntarr';
|
||
|
||
if (isValid) {
|
||
actionGroup.style.display = 'block';
|
||
deleteBtn.disabled = false;
|
||
deleteBtn.style.background = '#e74c3c';
|
||
deleteBtn.style.cursor = 'pointer';
|
||
} else {
|
||
actionGroup.style.display = 'none';
|
||
deleteBtn.disabled = true;
|
||
}
|
||
},
|
||
|
||
deleteDatabase: function() {
|
||
const confirmationInput = document.getElementById('delete-confirmation');
|
||
|
||
if (!confirmationInput || confirmationInput.value.toLowerCase() !== 'huntarr') {
|
||
this.showError('Please type "huntarr" to confirm database deletion');
|
||
return;
|
||
}
|
||
|
||
// Final confirmation dialog
|
||
if (!confirm('This will PERMANENTLY DELETE your entire Huntarr database. This action CANNOT be undone. Are you absolutely sure?')) {
|
||
return;
|
||
}
|
||
|
||
console.log('[BackupRestore] Deleting database');
|
||
|
||
const deleteBtn = document.getElementById('delete-database-btn');
|
||
if (deleteBtn) {
|
||
deleteBtn.disabled = true;
|
||
deleteBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deleting...';
|
||
}
|
||
|
||
fetch('./api/backup/delete-database', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
this.showSuccess('Database deleted successfully! Redirecting to setup...');
|
||
|
||
// Redirect to setup after a short delay
|
||
setTimeout(() => {
|
||
window.location.href = './setup';
|
||
}, 2000);
|
||
} else {
|
||
throw new Error(data.error || 'Failed to delete database');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('[BackupRestore] Error deleting database:', error);
|
||
this.showError('Failed to delete database: ' + error.message);
|
||
})
|
||
.finally(() => {
|
||
if (deleteBtn) {
|
||
deleteBtn.disabled = false;
|
||
deleteBtn.innerHTML = '<i class="fas fa-trash-alt"></i> Delete Database';
|
||
}
|
||
});
|
||
},
|
||
|
||
deleteBackup: function(backupId) {
|
||
if (!confirm('Are you sure you want to delete this backup? This action cannot be undone.')) {
|
||
return;
|
||
}
|
||
|
||
console.log('[BackupRestore] Deleting backup:', backupId);
|
||
|
||
fetch('./api/backup/delete', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
backup_id: backupId
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
this.showSuccess('Backup deleted successfully');
|
||
this.loadBackupList(); // Refresh the list
|
||
} else {
|
||
throw new Error(data.error || 'Failed to delete backup');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('[BackupRestore] Error deleting backup:', error);
|
||
this.showError('Failed to delete backup: ' + error.message);
|
||
});
|
||
},
|
||
|
||
formatFileSize: function(bytes) {
|
||
if (bytes === 0) return '0 Bytes';
|
||
|
||
const k = 1024;
|
||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
},
|
||
|
||
showSuccess: function(message) {
|
||
this.showNotification(message, 'success');
|
||
},
|
||
|
||
showError: function(message) {
|
||
this.showNotification(message, 'error');
|
||
},
|
||
|
||
showNotification: function(message, type) {
|
||
// Create notification element
|
||
const notification = document.createElement('div');
|
||
notification.className = `backup-notification ${type}`;
|
||
notification.innerHTML = `
|
||
<i class="fas ${type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'}"></i>
|
||
<span>${message}</span>
|
||
<button onclick="this.parentElement.remove()">×</button>
|
||
`;
|
||
|
||
// Add styles
|
||
notification.style.cssText = `
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
background: ${type === 'success' ? '#10b981' : '#e74c3c'};
|
||
color: white;
|
||
padding: 12px 16px;
|
||
border-radius: 6px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||
z-index: 10000;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
max-width: 400px;
|
||
animation: slideIn 0.3s ease;
|
||
`;
|
||
|
||
// Add animation styles
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
@keyframes slideIn {
|
||
from { transform: translateX(100%); opacity: 0; }
|
||
to { transform: translateX(0); opacity: 1; }
|
||
}
|
||
.backup-notification button {
|
||
background: none;
|
||
border: none;
|
||
color: white;
|
||
font-size: 18px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
margin-left: auto;
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
|
||
document.body.appendChild(notification);
|
||
|
||
// Auto-remove after 5 seconds
|
||
setTimeout(() => {
|
||
if (notification.parentElement) {
|
||
notification.remove();
|
||
}
|
||
}, 5000);
|
||
}
|
||
};
|
||
|
||
// Initialize when DOM is ready
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Don't auto-initialize - let the main UI handle it
|
||
console.log('[BackupRestore] Module loaded');
|
||
}); |