Files
Huntarr.io/frontend/static/js/modules/features/backup-restore.js
2026-02-10 10:41:38 -05:00

748 lines
29 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();
});
}
// Download backup selection
const downloadSelect = document.getElementById('download-backup-select');
if (downloadSelect) {
downloadSelect.addEventListener('change', () => {
this.handleDownloadSelection();
});
}
// Download backup button
const downloadBtn = document.getElementById('download-backup-btn');
if (downloadBtn) {
downloadBtn.addEventListener('click', () => {
this.downloadBackup();
});
}
// Upload backup file input
const uploadFileInput = document.getElementById('upload-backup-file');
if (uploadFileInput) {
uploadFileInput.addEventListener('change', () => {
this.handleUploadFileSelection();
});
}
// Upload confirmation input
const uploadConfirmation = document.getElementById('upload-confirmation');
if (uploadConfirmation) {
uploadConfirmation.addEventListener('input', () => {
this.validateUploadConfirmation();
});
}
// Upload button
const uploadBtn = document.getElementById('upload-backup-btn');
if (uploadBtn) {
uploadBtn.addEventListener('click', () => {
this.uploadBackup();
});
}
},
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);
// Ensure backup ID is properly escaped for HTML attributes
const escapedId = backup.id.replace(/'/g, "\\'");
html += `
<div class="backup-item" data-backup-id="${escapedId}">
<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('${escapedId}')">
<i class="fas fa-trash"></i> Delete
</button>
</div>
</div>
`;
});
listContainer.innerHTML = html;
},
populateRestoreSelect: function(backups) {
const restoreSelect = document.getElementById('restore-backup-select');
const downloadSelect = document.getElementById('download-backup-select');
if (!restoreSelect || !downloadSelect) return;
if (!backups || backups.length === 0) {
restoreSelect.innerHTML = '<option value="">No backups available</option>';
downloadSelect.innerHTML = '<option value="">No backups available</option>';
return;
}
let html = '<option value="">Select a backup to restore...</option>';
let downloadHtml = '<option value="">Select a backup to download...</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>`;
downloadHtml += `<option value="${backup.id}">${backup.name} - ${formattedDate} (${size})</option>`;
});
restoreSelect.innerHTML = html;
downloadSelect.innerHTML = downloadHtml;
},
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'
})
})
.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;
},
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;
}
var self = this;
var doRestore = function() {
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) {
self.showSuccess('Database restored successfully! Reloading in 3 seconds...');
setTimeout(() => {
window.location.reload();
}, 3000);
} else {
throw new Error(data.error || 'Failed to restore backup');
}
})
.catch(error => {
console.error('[BackupRestore] Error restoring backup:', error);
self.showError('Failed to restore backup: ' + error.message);
})
.finally(() => {
if (restoreBtn) {
restoreBtn.disabled = false;
restoreBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Restore Database';
}
});
};
if (window.HuntarrConfirm && window.HuntarrConfirm.show) {
window.HuntarrConfirm.show({ title: 'Restore Database', message: 'This will permanently overwrite your current database. Are you absolutely sure?', confirmLabel: 'Restore', onConfirm: doRestore });
} else {
if (!confirm('This will permanently overwrite your current database. Are you absolutely sure?')) return;
doRestore();
}
},
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;
} 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;
}
var self = this;
var doDelete = function() {
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) {
self.showSuccess('Database deleted successfully! Redirecting to setup...');
setTimeout(() => {
window.location.href = './setup';
}, 3000);
} else {
throw new Error(data.error || 'Failed to delete database');
}
})
.catch(error => {
console.error('[BackupRestore] Error deleting database:', error);
self.showError('Failed to delete database: ' + error.message);
})
.finally(() => {
if (deleteBtn) {
deleteBtn.disabled = false;
deleteBtn.innerHTML = '<i class="fas fa-trash-alt"></i> Delete Database';
}
});
};
if (window.HuntarrConfirm && window.HuntarrConfirm.show) {
window.HuntarrConfirm.show({ title: 'Delete Database', message: 'This will PERMANENTLY DELETE your entire Huntarr database. This action CANNOT be undone. Are you absolutely sure?', confirmLabel: 'Delete', onConfirm: doDelete });
} else {
if (!confirm('This will PERMANENTLY DELETE your entire Huntarr database. This action CANNOT be undone. Are you absolutely sure?')) return;
doDelete();
}
},
deleteBackup: function(backupId) {
var self = this;
var doDelete = function() {
console.log('[BackupRestore] Deleting backup:', backupId);
console.log('[BackupRestore] Backup ID type:', typeof backupId);
console.log('[BackupRestore] Backup ID length:', backupId ? backupId.length : 0);
// Add extra validation for backupId
if (!backupId || typeof backupId !== 'string') {
self.showError('Invalid backup ID provided for deletion');
return;
}
// Additional debugging - check if the backupId contains special characters
console.log('[BackupRestore] Backup ID raw:', backupId);
console.log('[BackupRestore] Backup ID escaped:', encodeURIComponent(backupId));
fetch('./api/backup/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
backup_id: backupId
})
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.success) {
self.showSuccess('Backup deleted successfully');
self.loadBackupList(); // Refresh the list
} else {
throw new Error(data.error || 'Failed to delete backup');
}
})
.catch(error => {
console.error('[BackupRestore] Error deleting backup:', error);
self.showError('Failed to delete backup: ' + error.message);
});
};
if (window.HuntarrConfirm && window.HuntarrConfirm.show) {
window.HuntarrConfirm.show({ title: 'Delete Backup', message: 'Are you sure you want to delete this backup? This action cannot be undone.', confirmLabel: 'Delete', onConfirm: doDelete });
} else {
if (!confirm('Are you sure you want to delete this backup? This action cannot be undone.')) return;
doDelete();
}
},
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) {
if (window.huntarrUI && window.huntarrUI.showNotification) {
window.huntarrUI.showNotification(message, 'success');
} else {
alert(message);
}
},
showError: function(message) {
if (window.huntarrUI && window.huntarrUI.showNotification) {
window.huntarrUI.showNotification(message, 'error');
} else {
alert(message);
}
},
// Download backup functions
handleDownloadSelection: function() {
const downloadSelect = document.getElementById('download-backup-select');
const downloadBtn = document.getElementById('download-backup-btn');
if (!downloadSelect || !downloadBtn) return;
if (downloadSelect.value) {
downloadBtn.disabled = false;
} else {
downloadBtn.disabled = true;
}
},
downloadBackup: function() {
const downloadSelect = document.getElementById('download-backup-select');
if (!downloadSelect) return;
const backupId = downloadSelect.value;
if (!backupId) {
this.showError('Please select a backup to download');
return;
}
console.log('[BackupRestore] Downloading backup:', backupId);
// Create a temporary link and trigger download
const downloadUrl = `./api/backup/download/${backupId}`;
const link = document.createElement('a');
link.href = downloadUrl;
link.download = `${backupId}.zip`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.showSuccess('Download started');
},
// Upload backup functions
handleUploadFileSelection: function() {
const uploadFileInput = document.getElementById('upload-backup-file');
const confirmationGroup = document.getElementById('upload-confirmation-group');
const actionGroup = document.getElementById('upload-action-group');
if (!uploadFileInput) return;
if (uploadFileInput.files.length > 0) {
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.validateUploadConfirmation();
},
validateUploadConfirmation: function() {
const confirmationInput = document.getElementById('upload-confirmation');
const uploadBtn = document.getElementById('upload-backup-btn');
if (!confirmationInput || !uploadBtn) return;
const isValid = confirmationInput.value.toUpperCase() === 'UPLOAD';
uploadBtn.disabled = !isValid;
},
uploadBackup: function() {
const uploadFileInput = document.getElementById('upload-backup-file');
const confirmationInput = document.getElementById('upload-confirmation');
if (!uploadFileInput || !confirmationInput) return;
const file = uploadFileInput.files[0];
const confirmation = confirmationInput.value.toUpperCase();
if (!file) {
this.showError('Please select a backup file to upload');
return;
}
if (confirmation !== 'UPLOAD') {
this.showError('Please type UPLOAD to confirm');
return;
}
var self = this;
var doUpload = function() {
console.log('[BackupRestore] Uploading backup:', file.name);
const uploadBtn = document.getElementById('upload-backup-btn');
if (uploadBtn) {
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Uploading and restoring...';
}
const formData = new FormData();
formData.append('backup_file', file);
fetch('./api/backup/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
self.showSuccess('Backup uploaded and restored successfully! Reloading in 3 seconds...');
setTimeout(() => {
window.location.reload();
}, 3000);
} else {
throw new Error(data.error || 'Failed to upload backup');
}
})
.catch(error => {
console.error('[BackupRestore] Error uploading backup:', error);
self.showError('Failed to upload backup: ' + error.message);
})
.finally(() => {
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Upload and Restore Backup';
}
});
};
if (window.HuntarrConfirm && window.HuntarrConfirm.show) {
window.HuntarrConfirm.show({ title: 'Upload and Restore', message: 'This will permanently overwrite your current database with the uploaded backup. Are you absolutely sure?', confirmLabel: 'Upload', onConfirm: doUpload });
} else {
if (!confirm('This will permanently overwrite your current database with the uploaded backup. Are you absolutely sure?')) return;
doUpload();
}
}
};
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
// Don't auto-initialize - let the main UI handle it
console.log('[BackupRestore] Module loaded');
});