569 lines
25 KiB
HTML
569 lines
25 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Backup & Restore{% endblock %}
|
|
|
|
{% block css %}
|
|
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div id="content" class="app-content">
|
|
<div class="container">
|
|
<ul class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
|
<li class="breadcrumb-item active">Backup & Restore</li>
|
|
</ul>
|
|
|
|
<div class="row align-items-center mb-3">
|
|
<div class="col">
|
|
<h1 class="page-header">Backup & Restore</h1>
|
|
<p class="text-muted">Manage system backups and data recovery operations</p>
|
|
</div>
|
|
<div class="col-auto">
|
|
<button type="button" class="btn btn-primary" onclick="createBackup()">
|
|
<i class="fa fa-plus me-2"></i>Create Backup
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Backup Status -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<div class="w-60px h-60px bg-primary bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
|
|
<i class="fa fa-database fa-2x text-primary"></i>
|
|
</div>
|
|
<h5>Total Backups</h5>
|
|
<div class="fs-24px fw-600 text-primary">{{ total_backups }}</div>
|
|
<div class="text-muted small">All time</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<div class="w-60px h-60px bg-success bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
|
|
<i class="fa fa-check-circle fa-2x text-success"></i>
|
|
</div>
|
|
<h5>Last Backup</h5>
|
|
<div class="fs-16px fw-600 text-success">{{ last_backup_date|date:"M d, Y" }}</div>
|
|
<div class="text-muted small">{{ last_backup_time_ago }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<div class="w-60px h-60px bg-info bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
|
|
<i class="fa fa-hdd fa-2x text-info"></i>
|
|
</div>
|
|
<h5>Storage Used</h5>
|
|
<div class="fs-20px fw-600 text-info">{{ total_backup_size }}</div>
|
|
<div class="text-muted small">{{ available_space }} available</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<div class="w-60px h-60px bg-warning bg-opacity-20 d-flex align-items-center justify-content-center rounded-circle mx-auto mb-3">
|
|
<i class="fa fa-clock fa-2x text-warning"></i>
|
|
</div>
|
|
<h5>Next Scheduled</h5>
|
|
<div class="fs-16px fw-600 text-warning">{{ next_backup_date|date:"M d, Y" }}</div>
|
|
<div class="text-muted small">{{ next_backup_time|date:"g:i A" }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Backup Configuration -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h4 class="card-title">Backup Configuration</h4>
|
|
</div>
|
|
<div class="card-body">
|
|
<form id="backupConfigForm">
|
|
{% csrf_token %}
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Automatic Backups</label>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" name="auto_backup_enabled"
|
|
{% if backup_config.auto_backup_enabled %}checked{% endif %}>
|
|
<label class="form-check-label">Enable automatic backups</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Backup Frequency</label>
|
|
<select name="backup_frequency" class="form-select">
|
|
<option value="daily" {% if backup_config.frequency == 'daily' %}selected{% endif %}>Daily</option>
|
|
<option value="weekly" {% if backup_config.frequency == 'weekly' %}selected{% endif %}>Weekly</option>
|
|
<option value="monthly" {% if backup_config.frequency == 'monthly' %}selected{% endif %}>Monthly</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Backup Time</label>
|
|
<input type="time" name="backup_time" class="form-control"
|
|
value="{{ backup_config.backup_time|default:'02:00' }}">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Retention Period</label>
|
|
<div class="input-group">
|
|
<input type="number" name="retention_days" class="form-control"
|
|
value="{{ backup_config.retention_days|default:30 }}" min="7" max="365">
|
|
<span class="input-group-text">days</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Backup Types</label>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="include_database"
|
|
{% if backup_config.include_database %}checked{% endif %}>
|
|
<label class="form-check-label">Database</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="include_media"
|
|
{% if backup_config.include_media %}checked{% endif %}>
|
|
<label class="form-check-label">Media files</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="include_logs"
|
|
{% if backup_config.include_logs %}checked{% endif %}>
|
|
<label class="form-check-label">System logs</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="include_config"
|
|
{% if backup_config.include_config %}checked{% endif %}>
|
|
<label class="form-check-label">Configuration files</label>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fa fa-save me-2"></i>Save Configuration
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h4 class="card-title">Quick Actions</h4>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-grid gap-3">
|
|
<button type="button" class="btn btn-outline-primary" onclick="createFullBackup()">
|
|
<i class="fa fa-database me-2"></i>Create Full Backup
|
|
</button>
|
|
|
|
<button type="button" class="btn btn-outline-info" onclick="createDatabaseBackup()">
|
|
<i class="fa fa-table me-2"></i>Database Only Backup
|
|
</button>
|
|
|
|
<button type="button" class="btn btn-outline-success" onclick="testBackupSystem()">
|
|
<i class="fa fa-check-circle me-2"></i>Test Backup System
|
|
</button>
|
|
|
|
<button type="button" class="btn btn-outline-warning" onclick="cleanupOldBackups()">
|
|
<i class="fa fa-trash me-2"></i>Cleanup Old Backups
|
|
</button>
|
|
|
|
<hr>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Restore from File</label>
|
|
<input type="file" class="form-control" id="restoreFile" accept=".sql,.zip,.tar.gz">
|
|
</div>
|
|
|
|
<button type="button" class="btn btn-outline-danger" onclick="restoreFromFile()">
|
|
<i class="fa fa-upload me-2"></i>Restore from File
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Backup History -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h4 class="card-title">Backup History</h4>
|
|
<div class="card-toolbar">
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="refreshBackupList()">
|
|
<i class="fa fa-refresh"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-striped" id="backupHistoryTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Backup Name</th>
|
|
<th>Type</th>
|
|
<th>Size</th>
|
|
<th>Created</th>
|
|
<th>Status</th>
|
|
<th>Duration</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for backup in backups %}
|
|
<tr>
|
|
<td>
|
|
<div class="fw-bold">{{ backup.name }}</div>
|
|
<div class="text-muted small">{{ backup.description|default:"No description" }}</div>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-{{ backup.type_color }}">
|
|
<i class="fa fa-{{ backup.type_icon }} me-1"></i>{{ backup.get_type_display }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="fw-bold">{{ backup.file_size_human }}</div>
|
|
{% if backup.compressed_size %}
|
|
<div class="text-muted small">{{ backup.compressed_size_human }} compressed</div>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div>{{ backup.created_at|date:"M d, Y" }}</div>
|
|
<div class="text-muted small">{{ backup.created_at|date:"g:i A" }}</div>
|
|
</td>
|
|
<td>
|
|
{% if backup.status == 'completed' %}
|
|
<span class="badge bg-success">
|
|
<i class="fa fa-check me-1"></i>Completed
|
|
</span>
|
|
{% elif backup.status == 'in_progress' %}
|
|
<span class="badge bg-warning">
|
|
<i class="fa fa-spinner fa-spin me-1"></i>In Progress
|
|
</span>
|
|
{% elif backup.status == 'failed' %}
|
|
<span class="badge bg-danger">
|
|
<i class="fa fa-times me-1"></i>Failed
|
|
</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">{{ backup.get_status_display }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if backup.duration %}
|
|
{{ backup.duration_human }}
|
|
{% else %}
|
|
<span class="text-muted">N/A</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="btn-group">
|
|
{% if backup.status == 'completed' %}
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="downloadBackup('{{ backup.id }}')">
|
|
<i class="fa fa-download"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-success btn-sm" onclick="restoreBackup('{{ backup.id }}')">
|
|
<i class="fa fa-undo"></i>
|
|
</button>
|
|
{% endif %}
|
|
<button type="button" class="btn btn-outline-info btn-sm" onclick="viewBackupDetails('{{ backup.id }}')">
|
|
<i class="fa fa-eye"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteBackup('{{ backup.id }}')">
|
|
<i class="fa fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Backup Progress Modal -->
|
|
<div class="modal fade" id="backupProgressModal" tabindex="-1" data-bs-backdrop="static">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Backup in Progress</h5>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="text-center mb-3">
|
|
<i class="fa fa-spinner fa-spin fa-3x text-primary"></i>
|
|
</div>
|
|
<div class="progress mb-3">
|
|
<div class="progress-bar" role="progressbar" style="width: 0%" id="backupProgress"></div>
|
|
</div>
|
|
<div class="text-center">
|
|
<div id="backupStatus">Initializing backup...</div>
|
|
<div class="text-muted small" id="backupDetails"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Restore Confirmation Modal -->
|
|
<div class="modal fade" id="restoreConfirmModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Confirm Restore</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-warning">
|
|
<i class="fa fa-exclamation-triangle me-2"></i>
|
|
<strong>Warning:</strong> This will overwrite current data with the backup data. This action cannot be undone.
|
|
</div>
|
|
<p>Are you sure you want to restore from this backup?</p>
|
|
<div id="restoreBackupInfo"></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-danger" id="confirmRestoreBtn">Restore</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
|
|
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
|
|
|
<script>
|
|
$(document).ready(function() {
|
|
// Initialize DataTable
|
|
$('#backupHistoryTable').DataTable({
|
|
responsive: true,
|
|
pageLength: 10,
|
|
order: [[3, 'desc']],
|
|
columnDefs: [
|
|
{ orderable: false, targets: [6] }
|
|
]
|
|
});
|
|
|
|
// Backup configuration form
|
|
$('#backupConfigForm').submit(function(e) {
|
|
e.preventDefault();
|
|
|
|
var formData = new FormData(this);
|
|
|
|
$.post('{% url "core:save_backup_config" %}', formData, function(response) {
|
|
if (response.success) {
|
|
toastr.success('Backup configuration saved');
|
|
} else {
|
|
toastr.error('Failed to save configuration: ' + response.error);
|
|
}
|
|
}).fail(function() {
|
|
toastr.error('Failed to save configuration');
|
|
});
|
|
});
|
|
});
|
|
|
|
function createBackup() {
|
|
createFullBackup();
|
|
}
|
|
|
|
function createFullBackup() {
|
|
if (confirm('Create a full system backup? This may take several minutes.')) {
|
|
startBackupProcess('full');
|
|
}
|
|
}
|
|
|
|
function createDatabaseBackup() {
|
|
if (confirm('Create a database backup?')) {
|
|
startBackupProcess('database');
|
|
}
|
|
}
|
|
|
|
function startBackupProcess(type) {
|
|
$('#backupProgressModal').modal('show');
|
|
|
|
$.post('{% url "core:create_backup" %}', {
|
|
'backup_type': type,
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
monitorBackupProgress(response.backup_id);
|
|
} else {
|
|
$('#backupProgressModal').modal('hide');
|
|
toastr.error('Failed to start backup: ' + response.error);
|
|
}
|
|
}).fail(function() {
|
|
$('#backupProgressModal').modal('hide');
|
|
toastr.error('Failed to start backup');
|
|
});
|
|
}
|
|
|
|
function monitorBackupProgress(backupId) {
|
|
var progressInterval = setInterval(function() {
|
|
$.get('{% url "core:backup_progress" 0 %}'.replace('0', backupId), function(response) {
|
|
$('#backupProgress').css('width', response.progress + '%');
|
|
$('#backupStatus').text(response.status);
|
|
$('#backupDetails').text(response.details);
|
|
|
|
if (response.completed) {
|
|
clearInterval(progressInterval);
|
|
$('#backupProgressModal').modal('hide');
|
|
|
|
if (response.success) {
|
|
toastr.success('Backup completed successfully');
|
|
refreshBackupList();
|
|
} else {
|
|
toastr.error('Backup failed: ' + response.error);
|
|
}
|
|
}
|
|
});
|
|
}, 2000);
|
|
}
|
|
|
|
function downloadBackup(backupId) {
|
|
window.location.href = '{% url "core:download_backup" 0 %}'.replace('0', backupId);
|
|
}
|
|
|
|
function restoreBackup(backupId) {
|
|
$.get('{% url "core:backup_detail" 0 %}'.replace('0', backupId), function(backup) {
|
|
$('#restoreBackupInfo').html(`
|
|
<strong>Backup:</strong> ${backup.name}<br>
|
|
<strong>Created:</strong> ${backup.created_at}<br>
|
|
<strong>Size:</strong> ${backup.file_size_human}<br>
|
|
<strong>Type:</strong> ${backup.type}
|
|
`);
|
|
|
|
$('#confirmRestoreBtn').off('click').on('click', function() {
|
|
performRestore(backupId);
|
|
});
|
|
|
|
$('#restoreConfirmModal').modal('show');
|
|
});
|
|
}
|
|
|
|
function performRestore(backupId) {
|
|
$('#restoreConfirmModal').modal('hide');
|
|
|
|
$.post('{% url "core:restore_backup" %}', {
|
|
'backup_id': backupId,
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
toastr.success('Restore completed successfully');
|
|
setTimeout(function() {
|
|
location.reload();
|
|
}, 2000);
|
|
} else {
|
|
toastr.error('Restore failed: ' + response.error);
|
|
}
|
|
}).fail(function() {
|
|
toastr.error('Failed to restore backup');
|
|
});
|
|
}
|
|
|
|
function restoreFromFile() {
|
|
var fileInput = document.getElementById('restoreFile');
|
|
if (!fileInput.files.length) {
|
|
toastr.warning('Please select a backup file first');
|
|
return;
|
|
}
|
|
|
|
if (confirm('Restore from uploaded file? This will overwrite current data.')) {
|
|
var formData = new FormData();
|
|
formData.append('backup_file', fileInput.files[0]);
|
|
formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');
|
|
|
|
$.ajax({
|
|
url: '{% url "core:restore_from_file" %}',
|
|
type: 'POST',
|
|
data: formData,
|
|
processData: false,
|
|
contentType: false,
|
|
success: function(response) {
|
|
if (response.success) {
|
|
toastr.success('Restore completed successfully');
|
|
setTimeout(function() {
|
|
location.reload();
|
|
}, 2000);
|
|
} else {
|
|
toastr.error('Restore failed: ' + response.error);
|
|
}
|
|
},
|
|
error: function() {
|
|
toastr.error('Failed to restore from file');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function viewBackupDetails(backupId) {
|
|
window.open('{% url "core:backup_detail" 0 %}'.replace('0', backupId), '_blank');
|
|
}
|
|
|
|
function deleteBackup(backupId) {
|
|
if (confirm('Delete this backup? This action cannot be undone.')) {
|
|
$.post('{% url "core:delete_backup" %}', {
|
|
'backup_id': backupId,
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
toastr.success('Backup deleted successfully');
|
|
refreshBackupList();
|
|
} else {
|
|
toastr.error('Failed to delete backup: ' + response.error);
|
|
}
|
|
}).fail(function() {
|
|
toastr.error('Failed to delete backup');
|
|
});
|
|
}
|
|
}
|
|
|
|
function testBackupSystem() {
|
|
$.post('{% url "core:test_backup_system" %}', {
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
toastr.success('Backup system test passed');
|
|
} else {
|
|
toastr.error('Backup system test failed: ' + response.error);
|
|
}
|
|
}).fail(function() {
|
|
toastr.error('Failed to test backup system');
|
|
});
|
|
}
|
|
|
|
function cleanupOldBackups() {
|
|
if (confirm('Delete backups older than the retention period?')) {
|
|
$.post('{% url "core:cleanup_old_backups" %}', {
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}, function(response) {
|
|
if (response.success) {
|
|
toastr.success(`Cleaned up ${response.deleted_count} old backups`);
|
|
refreshBackupList();
|
|
} else {
|
|
toastr.error('Failed to cleanup backups: ' + response.error);
|
|
}
|
|
}).fail(function() {
|
|
toastr.error('Failed to cleanup backups');
|
|
});
|
|
}
|
|
}
|
|
|
|
function refreshBackupList() {
|
|
location.reload();
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|