refactor: 🎨 remove caching #48
+2
-2
@@ -5,8 +5,8 @@ COPY . /app
|
|||||||
ARG VERSION=unknown
|
ARG VERSION=unknown
|
||||||
ENV VERSION=${VERSION}
|
ENV VERSION=${VERSION}
|
||||||
RUN pip install -r requirements.txt
|
RUN pip install -r requirements.txt
|
||||||
RUN apt-get update && apt-get install -y curl mariadb-client-compat
|
RUN apt-get update && apt-get install -y --no-install-recommends curl \
|
||||||
RUN rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
RUN curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 \
|
RUN curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 \
|
||||||
&& chmod +x tailwindcss-linux-x64 \
|
&& chmod +x tailwindcss-linux-x64 \
|
||||||
&& ./tailwindcss-linux-x64 -i ./static/css/input.css -o ./static/css/output.css --content "./templates/*.html,./static/js/*.min.js" --minify \
|
&& ./tailwindcss-linux-x64 -i ./static/css/input.css -o ./static/css/output.css --content "./templates/*.html,./static/js/*.min.js" --minify \
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ A Flask-based web application for comprehensive IP Address Management (IPAM). Ma
|
|||||||
docker run -d \
|
docker run -d \
|
||||||
--name ipam \
|
--name ipam \
|
||||||
-p 5000:5000 \
|
-p 5000:5000 \
|
||||||
-v ./backups:/app/backups \
|
|
||||||
-e MYSQL_HOST=10.10.2.27 \
|
-e MYSQL_HOST=10.10.2.27 \
|
||||||
-e MYSQL_USER=ipam \
|
-e MYSQL_USER=ipam \
|
||||||
-e MYSQL_PASSWORD=your_password \
|
-e MYSQL_PASSWORD=your_password \
|
||||||
@@ -61,8 +60,6 @@ services:
|
|||||||
- SECRET_KEY=your_secret_key
|
- SECRET_KEY=your_secret_key
|
||||||
- NAME=Your Organisation
|
- NAME=Your Organisation
|
||||||
- LOGO_PNG=https://example.com/logo.png
|
- LOGO_PNG=https://example.com/logo.png
|
||||||
volumes:
|
|
||||||
- ./backups:/app/backups
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import hashlib
|
|||||||
import base64
|
import base64
|
||||||
import secrets
|
import secrets
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
|
||||||
from io import StringIO, BytesIO
|
from io import StringIO, BytesIO
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@@ -20,7 +19,6 @@ import qrcode
|
|||||||
import requests
|
import requests
|
||||||
import mysql.connector
|
import mysql.connector
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from werkzeug.utils import secure_filename
|
|
||||||
from flask import (
|
from flask import (
|
||||||
Flask, session, request, redirect, url_for, abort, jsonify,
|
Flask, session, request, redirect, url_for, abort, jsonify,
|
||||||
render_template, send_from_directory, send_file, current_app,
|
render_template, send_from_directory, send_file, current_app,
|
||||||
@@ -2427,202 +2425,6 @@ def check_update():
|
|||||||
logging.error(f"Unexpected error checking for updates: {e}")
|
logging.error(f"Unexpected error checking for updates: {e}")
|
||||||
return jsonify({'error': 'Failed to check for updates'}), 500
|
return jsonify({'error': 'Failed to check for updates'}), 500
|
||||||
|
|
||||||
@app.route('/backup')
|
|
||||||
@permission_required('view_admin')
|
|
||||||
def backup():
|
|
||||||
"""Backup and restore page"""
|
|
||||||
from flask import current_app
|
|
||||||
|
|
||||||
# Ensure backups directory exists
|
|
||||||
backups_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'backups')
|
|
||||||
os.makedirs(backups_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# List available backups
|
|
||||||
backups = []
|
|
||||||
if os.path.exists(backups_dir):
|
|
||||||
for filename in os.listdir(backups_dir):
|
|
||||||
if filename.endswith('.sql'):
|
|
||||||
filepath = os.path.join(backups_dir, filename)
|
|
||||||
file_stat = os.stat(filepath)
|
|
||||||
backups.append({
|
|
||||||
'filename': filename,
|
|
||||||
'size': file_stat.st_size,
|
|
||||||
'created': datetime.fromtimestamp(file_stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
})
|
|
||||||
|
|
||||||
# Sort by creation time (newest first)
|
|
||||||
backups.sort(key=lambda x: x['created'], reverse=True)
|
|
||||||
|
|
||||||
return render_with_user('backup.html', backups=backups)
|
|
||||||
|
|
||||||
@app.route('/backup/create', methods=['POST'])
|
|
||||||
@permission_required('view_admin')
|
|
||||||
def create_backup():
|
|
||||||
"""Create a database backup"""
|
|
||||||
from flask import current_app
|
|
||||||
|
|
||||||
try:
|
|
||||||
backups_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'backups')
|
|
||||||
os.makedirs(backups_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# Generate backup filename with timestamp
|
|
||||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
||||||
filename = f'ipam_backup_{timestamp}.sql'
|
|
||||||
filepath = os.path.join(backups_dir, filename)
|
|
||||||
|
|
||||||
# Get database configuration
|
|
||||||
db_host = current_app.config['MYSQL_HOST']
|
|
||||||
db_user = current_app.config['MYSQL_USER']
|
|
||||||
db_password = current_app.config['MYSQL_PASSWORD']
|
|
||||||
db_name = current_app.config['MYSQL_DATABASE']
|
|
||||||
|
|
||||||
# Create backup using mysqldump
|
|
||||||
cmd = [
|
|
||||||
'mysqldump',
|
|
||||||
f'--host={db_host}',
|
|
||||||
f'--user={db_user}',
|
|
||||||
f'--password={db_password}',
|
|
||||||
'--skip-ssl',
|
|
||||||
'--single-transaction',
|
|
||||||
'--routines',
|
|
||||||
'--triggers',
|
|
||||||
db_name
|
|
||||||
]
|
|
||||||
|
|
||||||
with open(filepath, 'w') as f:
|
|
||||||
result = subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE, text=True)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
os.remove(filepath)
|
|
||||||
return jsonify({'error': f'Backup failed: {result.stderr}'}), 500
|
|
||||||
|
|
||||||
# Log the backup creation
|
|
||||||
with get_db_connection(current_app) as conn:
|
|
||||||
add_audit_log(session.get('user_id'), 'create_backup', f'Created backup: {filename}', conn=conn)
|
|
||||||
|
|
||||||
return jsonify({'success': True, 'filename': filename, 'message': 'Backup created successfully'})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error creating backup: {e}")
|
|
||||||
return jsonify({'error': f'Failed to create backup: {str(e)}'}), 500
|
|
||||||
|
|
||||||
@app.route('/backup/download/<filename>')
|
|
||||||
@permission_required('view_admin')
|
|
||||||
def download_backup(filename):
|
|
||||||
"""Download a backup file"""
|
|
||||||
from flask import current_app
|
|
||||||
|
|
||||||
# Security: ensure filename is safe
|
|
||||||
filename = secure_filename(filename)
|
|
||||||
backups_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'backups')
|
|
||||||
filepath = os.path.join(backups_dir, filename)
|
|
||||||
|
|
||||||
if not os.path.exists(filepath) or not filename.endswith('.sql'):
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
return send_file(filepath, as_attachment=True, download_name=filename)
|
|
||||||
|
|
||||||
@app.route('/backup/restore', methods=['POST'])
|
|
||||||
@permission_required('view_admin')
|
|
||||||
def restore_backup():
|
|
||||||
"""Restore database from backup"""
|
|
||||||
from flask import current_app
|
|
||||||
|
|
||||||
try:
|
|
||||||
backups_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'backups')
|
|
||||||
os.makedirs(backups_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# Check if file was uploaded or if using existing file
|
|
||||||
if 'backup_file' in request.files:
|
|
||||||
# Handle file upload
|
|
||||||
file = request.files['backup_file']
|
|
||||||
if file.filename == '':
|
|
||||||
return jsonify({'error': 'No file selected'}), 400
|
|
||||||
|
|
||||||
if not file.filename.endswith('.sql'):
|
|
||||||
return jsonify({'error': 'Invalid file type. Only .sql files are allowed'}), 400
|
|
||||||
|
|
||||||
# Save uploaded file
|
|
||||||
filename = secure_filename(file.filename)
|
|
||||||
filepath = os.path.join(backups_dir, filename)
|
|
||||||
file.save(filepath)
|
|
||||||
|
|
||||||
elif 'backup_filename' in request.form:
|
|
||||||
# Use existing backup file
|
|
||||||
filename = secure_filename(request.form['backup_filename'])
|
|
||||||
filepath = os.path.join(backups_dir, filename)
|
|
||||||
|
|
||||||
if not os.path.exists(filepath):
|
|
||||||
return jsonify({'error': 'Backup file not found'}), 404
|
|
||||||
else:
|
|
||||||
return jsonify({'error': 'No backup file specified'}), 400
|
|
||||||
|
|
||||||
# Get database configuration
|
|
||||||
db_host = current_app.config['MYSQL_HOST']
|
|
||||||
db_user = current_app.config['MYSQL_USER']
|
|
||||||
db_password = current_app.config['MYSQL_PASSWORD']
|
|
||||||
db_name = current_app.config['MYSQL_DATABASE']
|
|
||||||
|
|
||||||
# Close any existing database connections before restore
|
|
||||||
# This is important to avoid connection conflicts during restore
|
|
||||||
try:
|
|
||||||
# Try to close any open connections
|
|
||||||
pass
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Restore database using mysql command
|
|
||||||
cmd = [
|
|
||||||
'mysql',
|
|
||||||
f'--host={db_host}',
|
|
||||||
f'--user={db_user}',
|
|
||||||
f'--password={db_password}',
|
|
||||||
'--skip-ssl',
|
|
||||||
db_name
|
|
||||||
]
|
|
||||||
|
|
||||||
with open(filepath, 'r', encoding='utf-8') as f:
|
|
||||||
result = subprocess.run(cmd, stdin=f, stderr=subprocess.PIPE, text=True)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
return jsonify({'error': f'Restore failed: {result.stderr}'}), 500
|
|
||||||
|
|
||||||
# Log the restore
|
|
||||||
with get_db_connection(current_app) as conn:
|
|
||||||
add_audit_log(session.get('user_id'), 'restore_backup', f'Restored backup: {filename}', conn=conn)
|
|
||||||
|
|
||||||
return jsonify({'success': True, 'message': 'Database restored successfully'})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error restoring backup: {e}")
|
|
||||||
return jsonify({'error': f'Failed to restore backup: {str(e)}'}), 500
|
|
||||||
|
|
||||||
@app.route('/backup/delete/<filename>', methods=['POST'])
|
|
||||||
@permission_required('view_admin')
|
|
||||||
def delete_backup(filename):
|
|
||||||
"""Delete a backup file"""
|
|
||||||
from flask import current_app
|
|
||||||
|
|
||||||
# Security: ensure filename is safe
|
|
||||||
filename = secure_filename(filename)
|
|
||||||
backups_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'backups')
|
|
||||||
filepath = os.path.join(backups_dir, filename)
|
|
||||||
|
|
||||||
if not os.path.exists(filepath) or not filename.endswith('.sql'):
|
|
||||||
return jsonify({'error': 'Backup file not found'}), 404
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.remove(filepath)
|
|
||||||
|
|
||||||
# Log the deletion
|
|
||||||
with get_db_connection(current_app) as conn:
|
|
||||||
add_audit_log(session.get('user_id'), 'delete_backup', f'Deleted backup: {filename}', conn=conn)
|
|
||||||
|
|
||||||
return jsonify({'success': True, 'message': 'Backup deleted successfully'})
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error deleting backup: {e}")
|
|
||||||
return jsonify({'error': f'Failed to delete backup: {str(e)}'}), 500
|
|
||||||
|
|
||||||
@app.route('/get_available_ips')
|
@app.route('/get_available_ips')
|
||||||
@permission_required('view_device')
|
@permission_required('view_device')
|
||||||
def get_available_ips():
|
def get_available_ips():
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const messageDiv = document.getElementById('message');
|
|
||||||
|
|
||||||
function showMessage(text, isError = false) {
|
|
||||||
messageDiv.textContent = text;
|
|
||||||
messageDiv.className = isError
|
|
||||||
? 'mb-4 p-4 rounded-lg bg-red-200 dark:bg-red-800 text-red-800 dark:text-red-200'
|
|
||||||
: 'mb-4 p-4 rounded-lg bg-green-200 dark:bg-green-800 text-green-800 dark:text-green-200';
|
|
||||||
messageDiv.classList.remove('hidden');
|
|
||||||
setTimeout(() => {
|
|
||||||
messageDiv.classList.add('hidden');
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create backup button
|
|
||||||
const createBackupBtn = document.getElementById('create-backup-btn');
|
|
||||||
if (createBackupBtn) {
|
|
||||||
createBackupBtn.addEventListener('click', function() {
|
|
||||||
createBackupBtn.disabled = true;
|
|
||||||
createBackupBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating...';
|
|
||||||
|
|
||||||
fetch('/backup/create', {
|
|
||||||
method: 'POST'
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
showMessage(`Backup created successfully: ${data.filename}`);
|
|
||||||
setTimeout(() => window.location.reload(), 1500);
|
|
||||||
} else {
|
|
||||||
showMessage(data.error || 'Failed to create backup', true);
|
|
||||||
createBackupBtn.disabled = false;
|
|
||||||
createBackupBtn.innerHTML = '<i class="fas fa-database"></i> <span>Create Backup</span>';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
showMessage('Error creating backup: ' + error.message, true);
|
|
||||||
createBackupBtn.disabled = false;
|
|
||||||
createBackupBtn.innerHTML = '<i class="fas fa-database"></i> <span>Create Backup</span>';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload and restore form
|
|
||||||
const uploadRestoreForm = document.getElementById('upload-restore-form');
|
|
||||||
if (uploadRestoreForm) {
|
|
||||||
uploadRestoreForm.addEventListener('submit', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!confirm('WARNING: This will replace all current database data with the backup. Are you sure you want to continue?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData(this);
|
|
||||||
const submitBtn = this.querySelector('button[type="submit"]');
|
|
||||||
const originalText = submitBtn.innerHTML;
|
|
||||||
submitBtn.disabled = true;
|
|
||||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Restoring...';
|
|
||||||
|
|
||||||
fetch('/backup/restore', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
showMessage('Database restored successfully. Page will reload...');
|
|
||||||
setTimeout(() => window.location.reload(), 2000);
|
|
||||||
} else {
|
|
||||||
showMessage(data.error || 'Failed to restore backup', true);
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
submitBtn.innerHTML = originalText;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
showMessage('Error restoring backup: ' + error.message, true);
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
submitBtn.innerHTML = originalText;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Existing backup restore form
|
|
||||||
const existingRestoreForm = document.getElementById('existing-restore-form');
|
|
||||||
if (existingRestoreForm) {
|
|
||||||
existingRestoreForm.addEventListener('submit', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!confirm('WARNING: This will replace all current database data with the backup. Are you sure you want to continue?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData(this);
|
|
||||||
const submitBtn = this.querySelector('button[type="submit"]');
|
|
||||||
const originalText = submitBtn.innerHTML;
|
|
||||||
submitBtn.disabled = true;
|
|
||||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Restoring...';
|
|
||||||
|
|
||||||
fetch('/backup/restore', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
showMessage('Database restored successfully. Page will reload...');
|
|
||||||
setTimeout(() => window.location.reload(), 2000);
|
|
||||||
} else {
|
|
||||||
showMessage(data.error || 'Failed to restore backup', true);
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
submitBtn.innerHTML = originalText;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
showMessage('Error restoring backup: ' + error.message, true);
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
submitBtn.innerHTML = originalText;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function deleteBackup(filename) {
|
|
||||||
if (!confirm(`Are you sure you want to delete backup "${filename}"?`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(`/backup/delete/${filename}`, {
|
|
||||||
method: 'POST'
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
alert('Error: ' + (data.error || 'Failed to delete backup'));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
alert('Error: ' + error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
document.addEventListener("DOMContentLoaded",function(){let e=document.getElementById("message");function t(t,a=!1){e.textContent=t,e.className=a?"mb-4 p-4 rounded-lg bg-red-200 dark:bg-red-800 text-red-800 dark:text-red-200":"mb-4 p-4 rounded-lg bg-green-200 dark:bg-green-800 text-green-800 dark:text-green-200",e.classList.remove("hidden"),setTimeout(()=>{e.classList.add("hidden")},5e3)}let a=document.getElementById("create-backup-btn");a&&a.addEventListener("click",function(){a.disabled=!0,a.innerHTML='<i class="fas fa-spinner fa-spin"></i> Creating...',fetch("/backup/create",{method:"POST"}).then(e=>e.json()).then(e=>{e.success?(t(`Backup created successfully: ${e.filename}`),setTimeout(()=>window.location.reload(),1500)):(t(e.error||"Failed to create backup",!0),a.disabled=!1,a.innerHTML='<i class="fas fa-database"></i> <span>Create Backup</span>')}).catch(e=>{t("Error creating backup: "+e.message,!0),a.disabled=!1,a.innerHTML='<i class="fas fa-database"></i> <span>Create Backup</span>'})});let r=document.getElementById("upload-restore-form");r&&r.addEventListener("submit",function(e){if(e.preventDefault(),!confirm("WARNING: This will replace all current database data with the backup. Are you sure you want to continue?"))return;let a=new FormData(this),r=this.querySelector('button[type="submit"]'),n=r.innerHTML;r.disabled=!0,r.innerHTML='<i class="fas fa-spinner fa-spin"></i> Restoring...',fetch("/backup/restore",{method:"POST",body:a}).then(e=>e.json()).then(e=>{e.success?(t("Database restored successfully. Page will reload..."),setTimeout(()=>window.location.reload(),2e3)):(t(e.error||"Failed to restore backup",!0),r.disabled=!1,r.innerHTML=n)}).catch(e=>{t("Error restoring backup: "+e.message,!0),r.disabled=!1,r.innerHTML=n})});let n=document.getElementById("existing-restore-form");n&&n.addEventListener("submit",function(e){if(e.preventDefault(),!confirm("WARNING: This will replace all current database data with the backup. Are you sure you want to continue?"))return;let a=new FormData(this),r=this.querySelector('button[type="submit"]'),n=r.innerHTML;r.disabled=!0,r.innerHTML='<i class="fas fa-spinner fa-spin"></i> Restoring...',fetch("/backup/restore",{method:"POST",body:a}).then(e=>e.json()).then(e=>{e.success?(t("Database restored successfully. Page will reload..."),setTimeout(()=>window.location.reload(),2e3)):(t(e.error||"Failed to restore backup",!0),r.disabled=!1,r.innerHTML=n)}).catch(e=>{t("Error restoring backup: "+e.message,!0),r.disabled=!1,r.innerHTML=n})})});function deleteBackup(e){confirm(`Are you sure you want to delete backup "${e}"?`)&&fetch(`/backup/delete/${e}`,{method:"POST"}).then(e=>e.json()).then(e=>{e.success?window.location.reload():alert("Error: "+(e.error||"Failed to delete backup"))}).catch(e=>{alert("Error: "+e.message)})}
|
|
||||||
@@ -66,16 +66,6 @@
|
|||||||
<i class="fas fa-chevron-right text-gray-400"></i>
|
<i class="fas fa-chevron-right text-gray-400"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="/backup" class="bg-gray-200 dark:bg-zinc-800 hover:bg-gray-300 dark:hover:bg-zinc-700 p-6 rounded-lg shadow-md flex items-center justify-between transition-colors">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<i class="fas fa-database text-3xl text-gray-600 dark:text-gray-400"></i>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-lg font-bold">Backup & Restore</h3>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">Database backup and restore</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<i class="fas fa-chevron-right text-gray-400"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Subnet Management Section -->
|
<!-- Subnet Management Section -->
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Backup & Restore</title>
|
|
||||||
<link rel="icon" type="image/png" href="{{ LOGO_PNG }}">
|
|
||||||
<link href="/static/css/output.css" rel="stylesheet">
|
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body class="bg-gray-300 text-gray-900 dark:bg-zinc-900 dark:text-gray-100 min-h-screen flex flex-col">
|
|
||||||
{% include 'header.html' %}
|
|
||||||
<div class="flex-1 flex items-center justify-center mx-4">
|
|
||||||
<div class="container py-8 max-w-4xl pt-20">
|
|
||||||
<div class="flex items-center mb-6 relative">
|
|
||||||
<a href="/admin" class="hidden sm:flex absolute left-0 bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 items-center justify-center rounded-full w-11 h-11 hover:cursor-pointer"><i class="fas fa-arrow-left"></i></a>
|
|
||||||
<h1 class="text-3xl font-bold text-center w-full">Backup & Restore</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="message" class="hidden mb-4 p-4 rounded-lg"></div>
|
|
||||||
|
|
||||||
<!-- Create Backup Section -->
|
|
||||||
<div class="bg-gray-200 dark:bg-zinc-800 rounded-lg p-6 mb-6">
|
|
||||||
<h2 class="text-xl font-bold mb-4">Create Backup</h2>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Create a new database backup. This will export the entire database to a SQL file.</p>
|
|
||||||
<button id="create-backup-btn" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg flex items-center gap-2">
|
|
||||||
<i class="fas fa-database"></i>
|
|
||||||
<span>Create Backup</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Restore Backup Section -->
|
|
||||||
<div class="bg-gray-200 dark:bg-zinc-800 rounded-lg p-6 mb-6">
|
|
||||||
<h2 class="text-xl font-bold mb-4">Restore Backup</h2>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Restore the database from a backup file. <strong class="text-red-600 dark:text-red-400">Warning: This will replace all current data!</strong></p>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<!-- Upload Backup File -->
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-2">Upload Backup File</label>
|
|
||||||
<form id="upload-restore-form" enctype="multipart/form-data" class="flex gap-2">
|
|
||||||
<label class="flex-1 bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 border border-gray-600 rounded-lg px-4 py-2 cursor-pointer flex items-center justify-center hover:cursor-pointer">
|
|
||||||
<input type="file" name="backup_file" accept=".sql" required class="hidden" onchange="updateFileLabel(this)">
|
|
||||||
<span id="file-label" class="text-sm">Choose File</span>
|
|
||||||
</label>
|
|
||||||
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg">
|
|
||||||
<i class="fas fa-upload"></i> Upload & Restore
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Or Select Existing Backup -->
|
|
||||||
{% if backups %}
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-2">Or Restore from Existing Backup</label>
|
|
||||||
<form id="existing-restore-form" class="flex gap-2">
|
|
||||||
<select name="backup_filename" required class="flex-1 border p-2 rounded-lg bg-gray-300 dark:bg-zinc-900 border-gray-600">
|
|
||||||
<option value="">Select a backup...</option>
|
|
||||||
{% for backup in backups %}
|
|
||||||
<option value="{{ backup.filename }}">{{ backup.filename }} ({{ (backup.size / 1024 / 1024)|round(2) }} MB, {{ backup.created }})</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<button type="submit" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg">
|
|
||||||
<i class="fas fa-undo"></i> Restore
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Available Backups Section -->
|
|
||||||
<div class="bg-gray-200 dark:bg-zinc-800 rounded-lg p-6">
|
|
||||||
<h2 class="text-xl font-bold mb-4">Available Backups</h2>
|
|
||||||
{% if backups %}
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="w-full table-auto">
|
|
||||||
<thead>
|
|
||||||
<tr class="bg-gray-400 dark:bg-zinc-700">
|
|
||||||
<th class="px-4 py-2 text-left">Filename</th>
|
|
||||||
<th class="px-4 py-2 text-left">Size</th>
|
|
||||||
<th class="px-4 py-2 text-left">Created</th>
|
|
||||||
<th class="px-4 py-2 text-center">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for backup in backups %}
|
|
||||||
<tr class="border-b border-gray-700">
|
|
||||||
<td class="px-4 py-2">{{ backup.filename }}</td>
|
|
||||||
<td class="px-4 py-2">{{ (backup.size / 1024 / 1024)|round(2) }} MB</td>
|
|
||||||
<td class="px-4 py-2">{{ backup.created }}</td>
|
|
||||||
<td class="px-4 py-2 text-center">
|
|
||||||
<div class="flex gap-2 justify-center">
|
|
||||||
<a href="/backup/download/{{ backup.filename }}" class="bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-3 py-1 rounded text-sm" title="Download">
|
|
||||||
<i class="fas fa-download"></i>
|
|
||||||
</a>
|
|
||||||
<button onclick="deleteBackup('{{ backup.filename }}')" class="bg-red-300 hover:bg-red-400 dark:bg-red-700 dark:hover:bg-red-600 hover:cursor-pointer px-3 py-1 rounded text-sm" title="Delete">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p class="text-gray-600 dark:text-gray-400">No backups available. Create your first backup above.</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="/static/js/backup.min.js"></script>
|
|
||||||
<script>
|
|
||||||
function updateFileLabel(input) {
|
|
||||||
const label = document.getElementById('file-label');
|
|
||||||
if (input.files && input.files[0]) {
|
|
||||||
label.textContent = input.files[0].name;
|
|
||||||
} else {
|
|
||||||
label.textContent = 'Choose File';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user