refactor: 🎨 remove caching #48
+2
-2
@@ -5,8 +5,8 @@ COPY . /app
|
||||
ARG VERSION=unknown
|
||||
ENV VERSION=${VERSION}
|
||||
RUN pip install -r requirements.txt
|
||||
RUN apt-get update && apt-get install -y curl mariadb-client-compat
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/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 \
|
||||
|
||||
@@ -32,7 +32,6 @@ A Flask-based web application for comprehensive IP Address Management (IPAM). Ma
|
||||
docker run -d \
|
||||
--name ipam \
|
||||
-p 5000:5000 \
|
||||
-v ./backups:/app/backups \
|
||||
-e MYSQL_HOST=10.10.2.27 \
|
||||
-e MYSQL_USER=ipam \
|
||||
-e MYSQL_PASSWORD=your_password \
|
||||
@@ -61,8 +60,6 @@ services:
|
||||
- SECRET_KEY=your_secret_key
|
||||
- NAME=Your Organisation
|
||||
- LOGO_PNG=https://example.com/logo.png
|
||||
volumes:
|
||||
- ./backups:/app/backups
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -8,7 +8,6 @@ import hashlib
|
||||
import base64
|
||||
import secrets
|
||||
import logging
|
||||
import subprocess
|
||||
from io import StringIO, BytesIO
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
@@ -20,7 +19,6 @@ import qrcode
|
||||
import requests
|
||||
import mysql.connector
|
||||
from dotenv import load_dotenv
|
||||
from werkzeug.utils import secure_filename
|
||||
from flask import (
|
||||
Flask, session, request, redirect, url_for, abort, jsonify,
|
||||
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}")
|
||||
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')
|
||||
@permission_required('view_device')
|
||||
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>
|
||||
</a>
|
||||
{% 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>
|
||||
|
||||
<!-- 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