\x89PNG\r\n\x1a\n\x00\x00\x00\x0DIHDR\x00\x00\x00\x01\x00 \x00\x00\x01\x08\x06\x00\x00\x00\x1F\x15\xC4\x89\x00\x00\x00 \x0AIDATx\x9Ccb\x00\x00\x00\x06\x00\x03\x1A\x05\x9D\x00\x00 \x00\x00IEND\xAE\x42\x60\x82
| Path : /var/www/html/yedekledb24/DatabaseBackupMasterv8/ |
|
B-Con CMD Config cPanel C-Rdp D-Log Info Jump Mass Ransom Symlink vHost Zone-H |
| Current File : /var/www/html/yedekledb24/DatabaseBackupMasterv8/routes.py |
import os
import datetime
import psycopg2
from flask import render_template, request, redirect, url_for, flash, jsonify, session
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.sql.expression import text
from flask_babel import _, get_locale
from flask_login import login_user, logout_user, current_user, login_required
from app import app, db, logger, mail
from models import DatabaseConnection, StorageTarget, BackupJob, BackupLog, SmtpSettings, User
from forms import (DatabaseConnectionForm, StorageTargetForm, BackupJobForm, SmtpSettingsForm,
LoginForm, RegistrationForm, EncryptionKeyForm)
from backup_utils import test_database_connection, run_backup
from storage_utils import test_storage_connection, get_storage_space
from scheduler import schedule_backup_job, remove_scheduled_job
from restore_database import restore_from_backup
from database_performance import get_top_queries as pg_get_top_queries, get_database_stats as pg_get_database_stats
from database_performance import get_table_stats as pg_get_table_stats, enable_pg_stat_statements, pg_stat_statements_enabled
# MySQL performans fonksiyonlarını import et
try:
from mysql_performance import get_top_queries as mysql_get_top_queries, get_database_stats as mysql_get_database_stats
from mysql_performance import get_table_stats as mysql_get_table_stats, enable_performance_schema, performance_schema_enabled
MYSQL_PERFORMANCE_AVAILABLE = True
except ImportError:
MYSQL_PERFORMANCE_AVAILABLE = False
@app.route('/')
@login_required
def index():
"""Render the dashboard page with backup statistics."""
# Get recent backup logs
recent_logs = BackupLog.query.order_by(BackupLog.timestamp.desc()).limit(10).all()
# Get database connections
db_connections = DatabaseConnection.query.all()
# Get storage targets
storage_targets = StorageTarget.query.all()
# Get backup jobs
backup_jobs = BackupJob.query.all()
# Count success and failed backups
success_count = BackupLog.query.filter_by(status='success').count()
failed_count = BackupLog.query.filter_by(status='failed').count()
# Get storage space info
storage_info = []
for target in storage_targets:
try:
free_space, total_space = get_storage_space(target)
percent_free = (free_space / total_space) * 100 if total_space > 0 else 0
warning = percent_free < 10 # Warning if less than 10% space available
storage_info.append({
'id': target.id,
'name': target.name,
'free_space': free_space,
'total_space': total_space,
'percent_free': percent_free,
'warning': warning
})
except Exception as e:
logger.error("Error getting storage space for {}: {}".format(target.name, str(e)))
storage_info.append({
'id': target.id,
'name': target.name,
'free_space': 0,
'total_space': 0,
'percent_free': 0,
'warning': True,
'error': str(e)
})
return render_template('dashboard.html',
recent_logs=recent_logs,
db_count=len(db_connections),
storage_count=len(storage_targets),
job_count=len(backup_jobs),
success_count=success_count,
failed_count=failed_count,
storage_info=storage_info)
@app.route('/databases')
@login_required
def databases():
"""Render the database connections page."""
db_connections = DatabaseConnection.query.all()
form = DatabaseConnectionForm()
return render_template('databases.html', db_connections=db_connections, form=form)
@app.route('/databases/add', methods=['POST'])
@login_required
def add_database():
"""Add a new database connection."""
# Check if user has permission to manage database connections
if not current_user.can_manage_db_connections():
flash(_('Veritabanı bağlantısı ekleme izniniz yok.'), 'danger')
return redirect(url_for('databases'))
form = DatabaseConnectionForm()
if form.validate_on_submit():
try:
# Test connection before saving
connection_success = test_database_connection(
db_type=form.db_type.data,
host=form.host.data,
port=form.port.data,
username=form.username.data,
password=form.password.data,
database=form.database.data
)
if connection_success:
new_db = DatabaseConnection(
name=form.name.data,
db_type=form.db_type.data,
host=form.host.data,
port=form.port.data,
username=form.username.data,
password=form.password.data,
database=form.database.data,
pg_version=form.pg_version.data if form.db_type.data == 'postgresql' else None
)
db.session.add(new_db)
db.session.commit()
flash(_('Veritabanı bağlantısı başarıyla eklendi!'), 'success')
else:
flash(_('Veritabanına bağlanılamadı. Lütfen bilgilerinizi kontrol edin.'), 'danger')
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Database error: {str(e)}', 'danger')
except Exception as e:
flash(f'Error: {str(e)}', 'danger')
else:
for field, errors in form.errors.items():
for error in errors:
flash(f'{getattr(form, field).label.text}: {error}', 'danger')
return redirect(url_for('databases'))
@app.route('/databases/delete/<int:db_id>', methods=['POST'])
@login_required
def delete_database(db_id):
"""Delete a database connection."""
# Check if user has permission to manage database connections
if not current_user.can_manage_db_connections():
flash(_('Veritabanı bağlantısı silme izniniz yok.'), 'danger')
return redirect(url_for('databases'))
try:
db_conn = DatabaseConnection.query.get_or_404(db_id)
# Check if database is used in any backup job
jobs = BackupJob.query.filter_by(database_id=db_id).all()
if jobs:
flash(_('Yedekleme görevlerinde kullanılan veritabanı silinemez.'), 'danger')
return redirect(url_for('databases'))
db.session.delete(db_conn)
db.session.commit()
flash(_('Veritabanı bağlantısı başarıyla silindi!'), 'success')
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Veritabanı hatası: {str(e)}', 'danger')
except Exception as e:
flash(f'Hata: {str(e)}', 'danger')
return redirect(url_for('databases'))
@app.route('/databases/test/<int:db_id>')
@login_required
def test_database(db_id):
"""Test a database connection."""
# Test operations are allowed for all authenticated users
# since this is just reading data, not modifying
try:
db_conn = DatabaseConnection.query.get_or_404(db_id)
connection_success = test_database_connection(
db_type=db_conn.db_type,
host=db_conn.host,
port=db_conn.port,
username=db_conn.username,
password=db_conn.password,
database=db_conn.database
)
if connection_success:
flash(_('Veritabanı bağlantısı başarılı!'), 'success')
else:
flash(_('Veritabanına bağlanılamadı.'), 'danger')
except Exception as e:
flash(f'Veritabanı bağlantı testi hatası: {str(e)}', 'danger')
return redirect(url_for('databases'))
@app.route('/storage')
@login_required
def storage():
"""Render the storage targets page."""
storage_targets = StorageTarget.query.all()
form = StorageTargetForm()
# Get storage space info
for target in storage_targets:
try:
free_space, total_space = get_storage_space(target)
target.free_space = free_space
target.total_space = total_space
target.percent_free = (free_space / total_space) * 100 if total_space > 0 else 0
target.warning = target.percent_free < 10
except Exception as e:
logger.error("Error getting storage space for {}: {}".format(target.name, str(e)))
target.free_space = 0
target.total_space = 0
target.percent_free = 0
target.warning = True
target.error = str(e)
return render_template('storage.html', storage_targets=storage_targets, form=form)
@app.route('/storage/add', methods=['POST'])
@login_required
def add_storage():
"""Add a new storage target."""
# Check if user has permission to manage storage
if not current_user.can_manage_storage():
flash(_('Depolama hedefi ekleme izniniz yok.'), 'danger')
return redirect(url_for('storage'))
form = StorageTargetForm()
if form.validate_on_submit():
try:
# Test connection before saving
connection_success = test_storage_connection(
storage_type=form.storage_type.data,
host=form.host.data,
port=form.port.data,
username=form.username.data,
password=form.password.data,
path=form.path.data
)
if connection_success:
new_storage = StorageTarget(
name=form.name.data,
storage_type=form.storage_type.data,
host=form.host.data,
port=form.port.data,
username=form.username.data,
password=form.password.data,
path=form.path.data
)
db.session.add(new_storage)
db.session.commit()
flash(_('Depolama hedefi başarıyla eklendi!'), 'success')
else:
flash(_('Depolama hedefine bağlanılamadı. Lütfen kimlik bilgilerinizi kontrol edin.'), 'danger')
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Database error: {str(e)}', 'danger')
except Exception as e:
flash(f'Error: {str(e)}', 'danger')
else:
for field, errors in form.errors.items():
for error in errors:
flash(f'{getattr(form, field).label.text}: {error}', 'danger')
return redirect(url_for('storage'))
@app.route('/storage/delete/<int:storage_id>', methods=['POST'])
@login_required
def delete_storage(storage_id):
"""Delete a storage target."""
# Check if user has permission to manage storage
if not current_user.can_manage_storage():
flash(_('Depolama hedefi silme izniniz yok.'), 'danger')
return redirect(url_for('storage'))
try:
storage_target = StorageTarget.query.get_or_404(storage_id)
# Check if storage is used in any backup job
jobs = BackupJob.query.filter_by(storage_id=storage_id).all()
if jobs:
flash(_('Yedekleme görevlerinde kullanılan depolama hedefi silinemez.'), 'danger')
return redirect(url_for('storage'))
db.session.delete(storage_target)
db.session.commit()
flash(_('Depolama hedefi başarıyla silindi!'), 'success')
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Veritabanı hatası: {str(e)}', 'danger')
except Exception as e:
flash(f'Hata: {str(e)}', 'danger')
return redirect(url_for('storage'))
@app.route('/storage/test/<int:storage_id>')
@login_required
def test_storage(storage_id):
"""Test a storage connection."""
# Test operations are allowed for all authenticated users
# since this is just reading data, not modifying
try:
storage_target = StorageTarget.query.get_or_404(storage_id)
connection_success = test_storage_connection(
storage_type=storage_target.storage_type,
host=storage_target.host,
port=storage_target.port,
username=storage_target.username,
password=storage_target.password,
path=storage_target.path
)
if connection_success:
flash(_('Depolama hedefine bağlantı başarılı!'), 'success')
else:
flash(_('Depolama hedefine bağlanılamadı.'), 'danger')
except Exception as e:
flash(f'Depolama hedefi bağlantı testi hatası: {str(e)}', 'danger')
return redirect(url_for('storage'))
@app.route('/backup_jobs')
@login_required
def backup_jobs():
"""Render the backup jobs page."""
backup_jobs = BackupJob.query.all()
db_connections = DatabaseConnection.query.all()
storage_targets = StorageTarget.query.all()
form = BackupJobForm()
# Create a simple CSRF form for delete operations
from flask_wtf import FlaskForm
csrf_form = FlaskForm()
# Populate form choices
form.database_id.choices = [(db.id, db.name) for db in db_connections]
# Prepare database options with data-db-type attribute for the template
db_options = {}
for db in db_connections:
db_options[db.id] = db.db_type
form.storage_id.choices = [(s.id, s.name) for s in storage_targets]
# Encryption key choices if the table exists
try:
from models import EncryptionKey
encryption_keys = EncryptionKey.query.all()
form.encryption_key_id.choices = [(0, 'Anahtar Seçiniz...')] + [(k.id, f"{k.name} <{k.email}>") for k in encryption_keys]
except:
# Encryption key table might not exist yet
form.encryption_key_id.choices = [(0, 'Şifreleme Anahtarları Bulunamadı')]
# .env dosyasından varsayılan şifreyi al
default_passphrase = os.environ.get('BACKUP_ENCRYPTION_PSK', '')
if default_passphrase:
form.encryption_passphrase.data = default_passphrase
# Add information to backup jobs for display
for job in backup_jobs:
# Get the last backup log
job.last_log = BackupLog.query.filter_by(job_id=job.id).order_by(BackupLog.timestamp.desc()).first()
# Check if the next run is scheduled
job.next_run = None
try:
scheduler = app.scheduler
# APScheduler 3.x için uyumlu kod - get_jobs() artık bir liste döndürüyor
job_id = f"backup_job_{job.id}"
for scheduler_job in scheduler.get_jobs():
if scheduler_job.id == job_id and hasattr(scheduler_job, 'next_run_time'):
job.next_run = scheduler_job.next_run_time
break
except (AttributeError, Exception) as e:
# Scheduler erişilemiyorsa hata logla ama işleme devam et
logger.error("Zamanlayıcıya erişilirken hata oluştu: {}".format(str(e)))
# Hata durumunda job.next_run zaten None olarak kalır
return render_template('backup_jobs.html',
backup_jobs=backup_jobs,
db_connections=db_connections,
storage_targets=storage_targets,
form=form,
csrf_form=csrf_form)
@app.route('/backup_jobs/add', methods=['POST'])
@login_required
def add_backup_job():
"""Add a new backup job."""
# Check if user has permission to manage backup jobs
if not current_user.can_manage_backup_jobs():
flash(_('Yedekleme görevi ekleme izniniz yok.'), 'danger')
return redirect(url_for('backup_jobs'))
db_connections = DatabaseConnection.query.all()
storage_targets = StorageTarget.query.all()
form = BackupJobForm()
form.database_id.choices = [(db.id, db.name) for db in db_connections]
form.storage_id.choices = [(s.id, s.name) for s in storage_targets]
# Encryption key choices if the table exists
try:
from models import EncryptionKey
encryption_keys = EncryptionKey.query.all()
form.encryption_key_id.choices = [(0, 'Anahtar Seçiniz...')] + [(k.id, f"{k.name} <{k.email}>") for k in encryption_keys]
except:
# Encryption key table might not exist yet
form.encryption_key_id.choices = [(0, 'Şifreleme Anahtarları Bulunamadı')]
if form.validate_on_submit():
try:
# Base job properties
new_job = BackupJob(
name=form.name.data,
database_id=form.database_id.data,
storage_id=form.storage_id.data,
schedule_type=form.schedule_type.data,
schedule_value=form.schedule_value.data,
retention_days=form.retention_days.data,
enabled=form.enabled.data
)
# Add encryption properties if supported
try:
# If encryption_type field exists in the model, set it
if hasattr(new_job, 'encrypt_backup'):
new_job.encrypt_backup = form.encrypt_backup.data
new_job.encryption_type = form.encryption_type.data if form.encrypt_backup.data else None
# Set encryption key or passphrase based on encryption type
if form.encrypt_backup.data:
if form.encryption_type.data == 'asymmetric' and form.encryption_key_id.data > 0:
new_job.encryption_key_id = form.encryption_key_id.data
elif form.encryption_type.data == 'symmetric' and form.encryption_passphrase.data:
new_job.encryption_passphrase = form.encryption_passphrase.data
except:
# The encryption fields might not exist in the model yet
pass
# Add compression properties if supported
try:
# If compress_backup field exists in the model, set it
if hasattr(new_job, 'compress_backup'):
new_job.compress_backup = form.compress_backup.data
new_job.compression_type = form.compression_type.data if form.compress_backup.data else 'zip'
except:
# The compression fields might not exist in the model yet
pass
db.session.add(new_job)
db.session.commit()
# Schedule the job if enabled
if new_job.enabled:
schedule_backup_job(new_job)
flash(_('Yedekleme görevi başarıyla eklendi!'), 'success')
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Veritabanı hatası: {str(e)}', 'danger')
except Exception as e:
flash(f'Hata: {str(e)}', 'danger')
else:
for field, errors in form.errors.items():
for error in errors:
flash(f'{getattr(form, field).label.text}: {error}', 'danger')
return redirect(url_for('backup_jobs'))
@app.route('/backup_jobs/edit/<int:job_id>', methods=['GET', 'POST'])
@login_required
def edit_backup_job(job_id):
"""Edit an existing backup job."""
# Check if user has permission to manage backup jobs
if not current_user.can_manage_backup_jobs():
flash(_('Yedekleme görevi düzenleme izniniz yok.'), 'danger')
return redirect(url_for('backup_jobs'))
# Get the job to edit
job = BackupJob.query.get_or_404(job_id)
# Get database connections and storage targets
db_connections = DatabaseConnection.query.all()
storage_targets = StorageTarget.query.all()
# Create the form and populate with current job data
form = BackupJobForm(obj=job)
# Set form choices - database_id için data-db-type özniteliğini ekle
form.database_id.choices = [(db.id, db.name) for db in db_connections]
# Form choices özelliğine erişilemediği için, şablonda kullanmak için db_connections listesini kullanıyoruz
# Bu şekilde data-db-type özelliğini ekleyebiliriz
form.storage_id.choices = [(s.id, s.name) for s in storage_targets]
# Encryption key choices if the table exists
try:
from models import EncryptionKey
encryption_keys = EncryptionKey.query.all()
form.encryption_key_id.choices = [(0, 'Anahtar Seçiniz...')] + [(k.id, f"{k.name} <{k.email}>") for k in encryption_keys]
except:
# Encryption key table might not exist yet
form.encryption_key_id.choices = [(0, 'Şifreleme Anahtarları Bulunamadı')]
# Handle form submission
if request.method == 'POST' and form.validate_on_submit():
try:
# Update basic job properties
job.name = form.name.data
job.database_id = form.database_id.data
job.storage_id = form.storage_id.data
job.schedule_type = form.schedule_type.data
job.schedule_value = form.schedule_value.data
job.retention_days = form.retention_days.data
# Check if job enabled status changed
was_enabled = job.enabled
job.enabled = form.enabled.data
# Update encryption properties if supported
try:
if hasattr(job, 'encrypt_backup'):
job.encrypt_backup = form.encrypt_backup.data
job.encryption_type = form.encryption_type.data if form.encrypt_backup.data else None
# Update encryption key or passphrase based on encryption type
if form.encrypt_backup.data:
if form.encryption_type.data == 'asymmetric' and form.encryption_key_id.data > 0:
job.encryption_key_id = form.encryption_key_id.data
elif form.encryption_type.data == 'symmetric' and form.encryption_passphrase.data:
job.encryption_passphrase = form.encryption_passphrase.data
except:
# The encryption fields might not exist in the model yet
pass
# Update compression properties if supported
try:
if hasattr(job, 'compress_backup'):
job.compress_backup = form.compress_backup.data
job.compression_type = form.compression_type.data if form.compress_backup.data else 'zip'
except:
# The compression fields might not exist in the model yet
pass
# Update backup method properties if supported
try:
if hasattr(job, 'backup_method'):
job.backup_method = form.backup_method.data
# XtraBackup options
if hasattr(job, 'xtrabackup_options') and form.xtrabackup_options.data:
job.xtrabackup_options = form.xtrabackup_options.data
elif hasattr(job, 'xtrabackup_options'):
job.xtrabackup_options = None
except Exception as e:
# The backup method fields might not exist in the model yet
logger.error(f"Backup method update error: {str(e)}")
pass
# Save changes to database
db.session.commit()
# Update scheduler if needed
if was_enabled != job.enabled:
if job.enabled:
# Add to scheduler
schedule_backup_job(job)
else:
# Remove from scheduler
remove_scheduled_job(job.id)
elif job.enabled:
# Update existing scheduled job
remove_scheduled_job(job.id)
schedule_backup_job(job)
flash(_('Yedekleme görevi başarıyla güncellendi!'), 'success')
return redirect(url_for('backup_jobs'))
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Veritabanı hatası: {str(e)}', 'danger')
except Exception as e:
flash(f'Hata: {str(e)}', 'danger')
elif request.method == 'GET':
# Populate form with existing job data
form.name.data = job.name
form.database_id.data = job.database_id
form.storage_id.data = job.storage_id
form.schedule_type.data = job.schedule_type
form.schedule_value.data = job.schedule_value
form.retention_days.data = job.retention_days
form.enabled.data = job.enabled
# Set encryption form fields if they exist
try:
if hasattr(job, 'encrypt_backup'):
form.encrypt_backup.data = job.encrypt_backup
form.encryption_type.data = job.encryption_type or ''
if job.encryption_key_id:
form.encryption_key_id.data = job.encryption_key_id
# Şifre yoksa veya boşsa .env'den al
if job.encryption_passphrase:
form.encryption_passphrase.data = job.encryption_passphrase
else:
default_passphrase = os.environ.get('BACKUP_ENCRYPTION_PSK', '')
if default_passphrase:
form.encryption_passphrase.data = default_passphrase
except:
pass
# Set compression form fields if they exist
try:
if hasattr(job, 'compress_backup'):
form.compress_backup.data = job.compress_backup
form.compression_type.data = job.compression_type or 'zip'
except:
pass
# Set backup method form fields if they exist
try:
if hasattr(job, 'backup_method'):
form.backup_method.data = job.backup_method or 'full'
if hasattr(job, 'xtrabackup_options') and job.xtrabackup_options:
form.xtrabackup_options.data = job.xtrabackup_options
except Exception as e:
logger.error(f"Backup method field load error: {str(e)}")
pass
else:
for field, errors in form.errors.items():
for error in errors:
flash(f'{getattr(form, field).label.text}: {error}', 'danger')
return render_template('edit_backup_job.html',
form=form,
job=job,
db_connections=db_connections,
storage_targets=storage_targets)
@app.route('/backup_jobs/delete/<int:job_id>', methods=['POST'])
@login_required
def delete_backup_job(job_id):
"""Delete a backup job."""
# Check if user has permission to manage backup jobs
if not current_user.can_manage_backup_jobs():
flash(_('Yedekleme görevi silme izniniz yok.'), 'danger')
return redirect(url_for('backup_jobs'))
try:
# Silmeden önce logları temizleme yaklaşımı
# İlk olarak veritabanına direkt SQL sorgusu çalıştıralım
try:
# Log kayıtlarını doğrudan SQL ile silelim
sql = "DELETE FROM backup_log WHERE job_id = :job_id"
db.session.execute(text(sql), {"job_id": job_id})
db.session.commit()
except Exception as e:
db.session.rollback()
logger.error("Log kayıtları silinirken hata: {}".format(str(e)))
# Şimdi işi güvenle silebiliriz
job = BackupJob.query.get_or_404(job_id)
# Remove from scheduler if enabled
if job.enabled:
remove_scheduled_job(job.id)
# İşi sil
db.session.delete(job)
db.session.commit()
flash(_('Yedekleme görevi başarıyla silindi!'), 'success')
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Veritabanı hatası: {str(e)}', 'danger')
except Exception as e:
flash(f'Hata: {str(e)}', 'danger')
return redirect(url_for('backup_jobs'))
@app.route('/backup_jobs/toggle/<int:job_id>', methods=['POST'])
@login_required
def toggle_backup_job(job_id):
"""Toggle enabled status for a backup job."""
# Check if user has permission to manage backup jobs
if not current_user.can_manage_backup_jobs():
flash(_('Yedekleme görevi durumunu değiştirme izniniz yok.'), 'danger')
return redirect(url_for('backup_jobs'))
try:
job = BackupJob.query.get_or_404(job_id)
job.enabled = not job.enabled
if job.enabled:
# Add to scheduler
schedule_backup_job(job)
flash(_('Yedekleme görevi başarıyla etkinleştirildi!'), 'success')
else:
# Remove from scheduler
remove_scheduled_job(job.id)
flash(_('Yedekleme görevi başarıyla devre dışı bırakıldı!'), 'success')
db.session.commit()
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Veritabanı hatası: {str(e)}', 'danger')
except Exception as e:
flash(f'Hata: {str(e)}', 'danger')
return redirect(url_for('backup_jobs'))
@app.route('/backup_jobs/run/<int:job_id>', methods=['POST'])
@login_required
def run_backup_job(job_id):
"""Run a backup job manually."""
# Both admin and regular users should be able to run backups
# No explicit permission check since this is a read operation
# But we still check for authenticated users with login_required
try:
job = BackupJob.query.get_or_404(job_id)
# Run the backup with email notifications
success = run_backup(job, send_email=True)
if success:
flash(_('Yedekleme görevi başarıyla çalıştırıldı! E-posta bildirimleri gönderildi.'), 'success')
else:
flash(_('Yedekleme görevi çalıştırma başarısız oldu. Detaylar için logları kontrol edin.'), 'danger')
except Exception as e:
flash(f'Hata: {str(e)}', 'danger')
return redirect(url_for('backup_jobs'))
@app.route('/logs')
@login_required
def logs():
"""Render the backup logs page."""
page = request.args.get('page', 1, type=int)
per_page = 20
backup_logs = BackupLog.query.order_by(BackupLog.timestamp.desc()).paginate(page=page, per_page=per_page)
# Create a form for CSRF protection
from flask_wtf import FlaskForm
form = FlaskForm()
return render_template('logs.html', backup_logs=backup_logs, form=form)
@app.route('/restore/<int:log_id>', methods=['POST'])
@login_required
def restore_backup(log_id):
"""Restore a database from a backup."""
# Check if user has permission to manage backup jobs
if not current_user.can_manage_backup_jobs():
flash(_('Veritabanı geri yükleme izniniz yok.'), 'danger')
return redirect(url_for('logs'))
try:
# Find the backup log entry
backup_log = BackupLog.query.get_or_404(log_id)
# Make sure the backup was successful
if backup_log.status != 'success':
flash(_('Sadece başarılı yedekler geri yüklenebilir.'), 'danger')
return redirect(url_for('logs'))
# Make sure the backup file exists
if not backup_log.backup_file or not os.path.exists(backup_log.backup_file):
flash(_('Yedek dosyası bulunamadı.'), 'danger')
return redirect(url_for('logs'))
# Şifreli yedekler için şifre kontrolü
passphrase = request.form.get('passphrase', None)
# CSRF koruması için form oluşturulmalı
from flask_wtf import FlaskForm
form = FlaskForm()
# Perform the restore with passphrase if provided
result = restore_from_backup(log_id, passphrase)
# Şifre gerekiyorsa, şifre giriş formunu göster
if not result['success'] and result.get('needs_passphrase'):
return render_template('restore_password.html',
log_id=log_id,
job_name=backup_log.job.name,
database_name=backup_log.job.database.name,
form=form)
if result['success']:
# Create a new log entry for the restore
new_log = BackupLog(
job_id=backup_log.job_id,
status='success',
message=f"Geri yükleme başarılı: {os.path.basename(backup_log.backup_file)}",
backup_file=backup_log.backup_file,
backup_size=backup_log.backup_size,
duration=0 # We don't track duration for restores
)
db.session.add(new_log)
db.session.commit()
flash(_('Veritabanı başarıyla geri yüklendi!'), 'success')
else:
flash(_('Veritabanı geri yükleme hatası: ') + result['message'], 'danger')
except Exception as e:
logger.error("Restore error: {}".format(str(e)))
flash('Geri yükleme hatası: {}'.format(str(e)), 'danger')
return redirect(url_for('logs'))
@app.route('/settings')
@login_required
def settings():
"""Render the settings page."""
# Retrieve SMTP settings from database or environment variables
smtp_settings = SmtpSettings.get_settings()
# Create SMTP settings form
smtp_form = SmtpSettingsForm()
# Create encryption key form
encryption_form = EncryptionKeyForm()
# Get encryption keys if the table exists
encryption_keys = []
try:
from models import EncryptionKey
encryption_keys = EncryptionKey.query.all()
except:
# Encryption table might not exist yet
pass
# If there's an existing SMTP configuration in the database, populate the form
settings_record = SmtpSettings.query.first()
if settings_record:
# Default connection type based on port and SSL/TLS settings
connection_type = 'ssl' if settings_record.mail_use_ssl else 'tls'
# Populate form
smtp_form.mail_server.data = settings_record.mail_server
smtp_form.mail_username.data = settings_record.mail_username
smtp_form.mail_password.data = settings_record.mail_password
smtp_form.mail_default_sender.data = settings_record.mail_default_sender
smtp_form.mail_connection_type.data = connection_type
smtp_form.notification_emails.data = settings_record.notification_emails
return render_template('settings.html',
mail_settings=smtp_settings,
smtp_form=smtp_form,
encryption_form=encryption_form,
encryption_keys=encryption_keys)
@app.route('/settings/smtp', methods=['POST'])
@login_required
def save_smtp_settings():
"""Save SMTP email settings."""
# Only admin users can modify SMTP settings
if not current_user.is_admin:
flash(_('E-posta ayarlarını değiştirme izniniz yok.'), 'danger')
return redirect(url_for('settings'))
form = SmtpSettingsForm()
if form.validate_on_submit():
try:
# Determine SSL/TLS based on connection type
is_tls = form.mail_connection_type.data == 'tls'
is_ssl = form.mail_connection_type.data == 'ssl'
# Set port based on connection type if not explicitly provided
port = 587 if is_tls else 465
# Check if we have existing settings to update
settings = SmtpSettings.query.first()
if settings:
# Update existing record
settings.mail_server = form.mail_server.data
settings.mail_port = port
settings.mail_use_tls = is_tls
settings.mail_use_ssl = is_ssl
settings.mail_username = form.mail_username.data
settings.mail_password = form.mail_password.data
settings.mail_default_sender = form.mail_default_sender.data
settings.notification_emails = form.notification_emails.data
else:
# Create new record
settings = SmtpSettings(
mail_server=form.mail_server.data,
mail_port=port,
mail_use_tls=is_tls,
mail_use_ssl=is_ssl,
mail_username=form.mail_username.data,
mail_password=form.mail_password.data,
mail_default_sender=form.mail_default_sender.data,
notification_emails=form.notification_emails.data
)
db.session.add(settings)
# Save to database
db.session.commit()
# Update mail configuration
app.config['MAIL_SERVER'] = settings.mail_server
app.config['MAIL_PORT'] = settings.mail_port
app.config['MAIL_USE_TLS'] = settings.mail_use_tls
app.config['MAIL_USE_SSL'] = settings.mail_use_ssl
app.config['MAIL_USERNAME'] = settings.mail_username
app.config['MAIL_PASSWORD'] = settings.mail_password
app.config['MAIL_DEFAULT_SENDER'] = settings.mail_default_sender
# Update environment variables for compatibility with existing code
os.environ['MAIL_SERVER'] = settings.mail_server
os.environ['MAIL_PORT'] = str(settings.mail_port)
os.environ['MAIL_USE_TLS'] = str(settings.mail_use_tls)
os.environ['MAIL_USE_SSL'] = str(settings.mail_use_ssl)
os.environ['MAIL_USERNAME'] = settings.mail_username
os.environ['MAIL_PASSWORD'] = settings.mail_password
os.environ['MAIL_DEFAULT_SENDER'] = settings.mail_default_sender
# Update environment variable for mail notifications
os.environ['BACKUP_NOTIFICATION_EMAILS'] = settings.notification_emails
# Reinitialize mail
mail.init_app(app)
flash(_('E-posta ayarları başarıyla kaydedildi!'), 'success')
except Exception as e:
db.session.rollback()
flash(f'E-posta ayarlarını kaydederken hata: {str(e)}', 'danger')
else:
for field, errors in form.errors.items():
for error in errors:
flash(f'{getattr(form, field).label.text}: {error}', 'danger')
return redirect(url_for('settings'))
@app.route('/settings/encryption_key/add', methods=['POST'])
@login_required
def add_encryption_key():
"""Create a new GPG encryption key."""
# Only admin users can create encryption keys
if not current_user.is_admin:
flash(_('GPG anahtarı oluşturma izniniz yok.'), 'danger')
return redirect(url_for('settings'))
form = EncryptionKeyForm()
if form.validate_on_submit():
try:
# Import encryption utils
from encryption_utils import generate_gpg_key
# Generate GPG key pair
key_result = generate_gpg_key(
name=form.name.data,
email=form.email.data,
passphrase=form.passphrase.data if form.passphrase.data else None,
key_type=form.key_type.data,
key_length=form.key_length.data
)
if key_result:
# Create database record
from models import EncryptionKey
new_key = EncryptionKey(
name=form.name.data,
email=form.email.data,
key_type=form.key_type.data,
key_length=form.key_length.data,
fingerprint=key_result.get('fingerprint', ''),
public_key=key_result.get('public_key', ''),
private_key=key_result.get('private_key', '') if form.store_private_key.data else None,
created_by=current_user.id
)
db.session.add(new_key)
db.session.commit()
flash(_('GPG şifreleme anahtarı başarıyla oluşturuldu!'), 'success')
else:
flash(_('GPG anahtarı oluşturulamadı.'), 'danger')
except Exception as e:
db.session.rollback()
flash(f'GPG anahtarı oluşturma hatası: {str(e)}', 'danger')
else:
for field, errors in form.errors.items():
for error in errors:
flash(f'{getattr(form, field).label.text}: {error}', 'danger')
return redirect(url_for('settings'))
@app.route('/settings/encryption_key/delete/<int:key_id>', methods=['POST'])
@login_required
def delete_encryption_key(key_id):
"""Delete a GPG encryption key."""
# Only admin users can delete encryption keys
if not current_user.is_admin:
flash(_('GPG anahtarı silme izniniz yok.'), 'danger')
return redirect(url_for('settings'))
try:
# Import encryption model
from models import EncryptionKey
encryption_key = EncryptionKey.query.get_or_404(key_id)
# Check if key is used in any backup job
jobs = BackupJob.query.filter_by(encryption_key_id=key_id).all()
if jobs:
flash(_('Yedekleme görevlerinde kullanılan GPG anahtarı silinemez.'), 'danger')
return redirect(url_for('settings'))
# Delete the key
db.session.delete(encryption_key)
db.session.commit()
flash(_('GPG anahtarı başarıyla silindi!'), 'success')
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Veritabanı hatası: {str(e)}', 'danger')
except Exception as e:
flash(f'Hata: {str(e)}', 'danger')
return redirect(url_for('settings'))
@app.route('/settings/test_email', methods=['POST'])
@login_required
def test_email():
"""Send a test email to verify email settings."""
# Only admin users can send test emails
if not current_user.is_admin:
flash(_('Test e-postası gönderme izniniz yok.'), 'danger')
return redirect(url_for('settings'))
try:
# Get email recipient(s)
recipients_str = request.form.get('email_addresses', '')
if not recipients_str:
flash(_('Lütfen test e-postası göndermek için en az bir e-posta adresi girin.'), 'danger')
return redirect(url_for('settings'))
recipients = [email.strip() for email in recipients_str.split(',')]
# Import and call the test email function
from email_utils import send_test_email
success = send_test_email(recipients)
if success:
flash(_('Test e-postası başarıyla gönderildi! Lütfen gelen kutunuzu kontrol edin.'), 'success')
else:
flash(_('Test e-postası gönderilemedi. Lütfen e-posta ayarlarınızı kontrol edin.'), 'danger')
except Exception as e:
flash(f'Test e-postası gönderme hatası: {str(e)}', 'danger')
return redirect(url_for('settings'))
@app.route('/api/dashboard_stats')
def dashboard_stats():
"""API endpoint for dashboard statistics."""
# Get backup success/failure counts for the last 7 days
from sqlalchemy import func
import datetime
# Calculate date 7 days ago
seven_days_ago = datetime.datetime.now() - datetime.timedelta(days=7)
# Query logs to get counts by day and status
daily_stats = db.session.query(
func.date(BackupLog.timestamp).label('date'),
BackupLog.status,
func.count().label('count')
).filter(
BackupLog.timestamp >= seven_days_ago
).group_by(
func.date(BackupLog.timestamp),
BackupLog.status
).all()
# Format the data for Chart.js
dates = sorted(list(set([stat.date.strftime('%Y-%m-%d') for stat in daily_stats])))
success_data = [0] * len(dates)
failed_data = [0] * len(dates)
for stat in daily_stats:
date_index = dates.index(stat.date.strftime('%Y-%m-%d'))
if stat.status == 'success':
success_data[date_index] = stat.count
else:
failed_data[date_index] = stat.count
return jsonify({
'labels': dates,
'success': success_data,
'failed': failed_data
})
# Language and Theme Routes
@app.route('/set_language/<lang>')
def set_language(lang):
"""Set the user's preferred language."""
if lang in ['en', 'tr']:
session['lang'] = lang
flash(_('Language changed successfully!'), 'success')
return redirect(request.referrer or url_for('index'))
@app.route('/set_theme/<theme>')
def set_theme(theme):
"""Set the user's preferred theme."""
# Tema değiştirme kaldırıldı, sadece light tema kullanılıyor
session['theme'] = 'light'
return redirect(request.referrer or url_for('index'))
@app.route('/login', methods=['GET', 'POST'])
def login():
"""Login page."""
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash(_('Geçersiz kullanıcı adı veya şifre'), 'danger')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
user.last_login = datetime.datetime.utcnow()
db.session.commit()
next_page = request.args.get('next')
if not next_page or not next_page.startswith('/'):
next_page = url_for('index')
return redirect(next_page)
return render_template('login.html', title=_('Giriş Yap'), form=form)
@app.route('/logout')
def logout():
"""Logout the current user."""
logout_user()
return redirect(url_for('login'))
# User management routes
@app.route('/users')
@login_required
def users():
"""Render the user management page."""
# Only admins can access this page
if not current_user.is_admin:
flash(_('Bu sayfaya erişim izniniz yok.'), 'danger')
return redirect(url_for('index'))
users = User.query.all()
form = RegistrationForm()
return render_template('users.html',
users=users,
form=form,
current_user=current_user)
@app.route('/users/add', methods=['POST'])
@login_required
def add_user():
"""Add a new user."""
# Only admins can add users
if not current_user.is_admin:
flash(_('Kullanıcı ekleme izniniz yok.'), 'danger')
return redirect(url_for('index'))
form = RegistrationForm()
if form.validate_on_submit():
try:
new_user = User(
username=form.username.data,
email=form.email.data,
is_admin=form.is_admin.data
)
new_user.set_password(form.password.data)
db.session.add(new_user)
db.session.commit()
flash(_('Kullanıcı başarıyla eklendi!'), 'success')
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Veritabanı hatası: {str(e)}', 'danger')
except Exception as e:
flash(f'Hata: {str(e)}', 'danger')
else:
for field, errors in form.errors.items():
for error in errors:
flash(f'{getattr(form, field).label.text}: {error}', 'danger')
return redirect(url_for('users'))
@app.route('/users/delete/<int:user_id>', methods=['POST'])
@login_required
def delete_user(user_id):
"""Delete a user."""
# Only admins can delete users
if not current_user.is_admin:
flash(_('Kullanıcı silme izniniz yok.'), 'danger')
return redirect(url_for('index'))
try:
user = User.query.get_or_404(user_id)
# Prevent deleting the admin user or yourself
if user.username == 'admin':
flash(_('Ana admin kullanıcısı silinemez.'), 'danger')
return redirect(url_for('users'))
if user.id == current_user.id:
flash(_('Kendinizi silemezsiniz.'), 'danger')
return redirect(url_for('users'))
db.session.delete(user)
db.session.commit()
flash(_('Kullanıcı başarıyla silindi!'), 'success')
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Veritabanı hatası: {str(e)}', 'danger')
except Exception as e:
flash(f'Hata: {str(e)}', 'danger')
return redirect(url_for('users'))
@app.route('/users/toggle_admin/<int:user_id>', methods=['POST'])
@login_required
def toggle_user_admin(user_id):
"""Toggle admin status for a user."""
# Only admins can change admin status
if not current_user.is_admin:
flash(_('Yetki değiştirme izniniz yok.'), 'danger')
return redirect(url_for('index'))
try:
user = User.query.get_or_404(user_id)
# Prevent changing admin status of the main admin user
if user.username == 'admin':
flash(_('Ana admin kullanıcısının yetkisi değiştirilemez.'), 'danger')
return redirect(url_for('users'))
user.is_admin = not user.is_admin
db.session.commit()
if user.is_admin:
flash(f'{user.username} kullanıcısına admin yetkisi verildi.', 'success')
else:
flash(f'{user.username} kullanıcısının admin yetkisi kaldırıldı.', 'success')
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Veritabanı hatası: {str(e)}', 'danger')
except Exception as e:
flash(f'Hata: {str(e)}', 'danger')
return redirect(url_for('users'))
# Register endpoint kaldırıldı
# Alternatif olarak, ilk kullanıcıyı oluşturmak için bir komut satırı aracı:
def create_admin_user(username, email, password):
"""
Komut satırı aracından çağrılmak üzere tasarlanmıştır.
İlk admin kullanıcıyı oluşturur.
"""
# İlk kullanıcı kontrolü
if User.query.count() > 0:
return False, "Sistemde zaten kullanıcılar bulunuyor!"
try:
user = User(username=username, email=email, is_admin=True)
user.set_password(password)
db.session.add(user)
db.session.commit()
return True, f"Admin kullanıcı {username} başarıyla oluşturuldu!"
except Exception as e:
db.session.rollback()
return False, f"Hata: {str(e)}"
# Veritabanı Performans Yönetimi Sayfaları
@app.route('/performance')
@login_required
def performance_dashboard():
"""Render the performance dashboard page."""
# Desteklenen veritabanlarını getir
db_connections = []
# PostgreSQL veritabanlarını getir
pg_dbs = DatabaseConnection.query.filter_by(db_type='postgresql').all()
for db in pg_dbs:
db.performance_supported = True
db.performance_type = 'postgresql'
db_connections.append(db)
# MySQL/MariaDB veritabanlarını getir (performans desteği mevcutsa)
if MYSQL_PERFORMANCE_AVAILABLE:
mysql_dbs = DatabaseConnection.query.filter(
(DatabaseConnection.db_type == 'mysql') |
(DatabaseConnection.db_type == 'mariadb')
).all()
for db in mysql_dbs:
db.performance_supported = True
db.performance_type = 'mysql'
db_connections.append(db)
# Create a simple CSRF form for database operations
from flask_wtf import FlaskForm
csrf_form = FlaskForm()
return render_template('performance/dashboard.html',
db_connections=db_connections,
csrf_form=csrf_form,
mysql_supported=MYSQL_PERFORMANCE_AVAILABLE)
@app.route('/performance/queries/<int:db_id>')
@login_required
def performance_queries(db_id):
"""Veritabanı sorgu performans verilerini göster."""
# Doğrudan grafik görünümüne yönlendir (tüm veritabanları için)
if request.args.get('view', 'chart') == 'chart':
return redirect(url_for('performance_charts', db_id=db_id))
try:
# Veritabanı bağlantısını al
db_conn = DatabaseConnection.query.get_or_404(db_id)
# Sorgu parametrelerini al
limit = request.args.get('limit', 20, type=int)
order_by = request.args.get('order_by', 'total_time')
interval = request.args.get('interval', None)
# Veritabanı bilgilerini hazırla
db_info = {
'host': db_conn.host,
'port': db_conn.port,
'username': db_conn.username,
'password': db_conn.password,
'database': db_conn.database
}
# Veritabanı tipine göre uygun fonksiyonu çağır
if db_conn.db_type == 'postgresql':
# Sabit formatlama kullanan database_performance_fixed modülünü kullan
from database_performance_fixed import get_top_queries as pg_get_top_queries
result = pg_get_top_queries(db_info, limit, order_by, interval)
# pg_stat_statements etkin değilse bilgi mesajı göster
if not result.get('success', False) and 'pg_stat_statements modülü etkin değil' in result.get('message', ''):
# CSRF form for module activation
from flask_wtf import FlaskForm
csrf_form = FlaskForm()
return render_template('performance/missing_pgss.html',
db_conn=db_conn,
message=result.get('message', ''),
csrf_form=csrf_form)
elif db_conn.db_type in ['mysql', 'mariadb'] and MYSQL_PERFORMANCE_AVAILABLE:
# mysql_performance modülünden import edilmiş fonksiyonları kullan
from mysql_performance import get_top_queries as mysql_get_top_queries
result = mysql_get_top_queries(db_info, limit, order_by, interval)
# Performance Schema etkin değilse bilgi mesajı göster
if not result.get('success', False) and 'Performance Schema etkin değil' in result.get('message', ''):
# CSRF form for module activation
from flask_wtf import FlaskForm
csrf_form = FlaskForm()
return render_template('performance/missing_pgss.html',
db_conn=db_conn,
message=result.get('message', ''),
csrf_form=csrf_form)
else:
flash(_('Sorgu performansı bu veritabanı türü için desteklenmiyor.'), 'warning')
return redirect(url_for('performance_dashboard'))
# Hata durumunda bilgi mesajı göster
if not result.get('success', False):
flash(result.get('message', 'Bilinmeyen bir hata oluştu'), 'danger')
return redirect(url_for('performance_dashboard'))
# Veri varsa sonuçları render et
pg_version = None
mysql_version = None
if db_conn.db_type == 'postgresql':
pg_version = result.get('pg_version', 'N/A') # pg_version anahtarını kullan
elif db_conn.db_type in ['mysql', 'mariadb']:
mysql_version = result.get('version', 'N/A')
# `summary` nesnesinde None değer olma ihtimaline karşı güvenlik önlemi
summary = result.get('summary', {})
if summary is None:
summary = {}
# Debug için loglar
app.logger.info(f"Sorgu sonucu anahtarları: {result.keys()}")
app.logger.info(f"PostgreSQL versiyonu: {pg_version}")
app.logger.info(f"Özet veri: {summary}")
# Veri alanlarını kontrol et ve hata durumunda detaylı log
try:
for query_data in result.get('data', []):
# Kilit alanları kontrol et
app.logger.info(f"Sorgu veri örneği: {query_data}")
break # Sadece ilk öğeyi log'la
except Exception as e:
app.logger.error(f"Sorgu verisi işlenirken hata: {e}")
# Format ve güvenlik uyarısı varsa kullanıcıya daha bilgi verici bir mesaj göster
if not result.get('data'):
flash('Sorgu performans verileri alındı ancak sonuç bulunamadı. SQL sorguları henüz kaydedilmemiş olabilir.', 'warning')
try:
# Tüm veriyi doğru formatta olduğundan emin olacak şekilde ön-işle
processed_queries = []
chart_data = {
"labels": [], # Sorgu metinlerinden kısaltılmış etiketler
"cpu_percentages": [], # CPU yüzdeleri (sayısal değerler)
"time_values": [], # Toplam çalışma süresi (ms cinsinden sayısal değerler)
"calls": [] # Çağrı sayıları
}
for query in result.get('data', []):
# Orijinal veriyi değiştirmemek için kopyasını al
processed_query = query.copy()
# 1. Tüm alanları ön-kontrol ve temizleme
for key in processed_query:
if processed_query[key] is None:
# None değerler için varsayılan değerler
if key in ['total_time', 'avg_time']:
processed_query[key] = "0.00 ms"
elif key == 'percentage_cpu':
processed_query[key] = "0.00%"
elif key in ['calls', 'rows']:
processed_query[key] = 0
elif key == 'query':
processed_query[key] = "Unknown query"
else:
processed_query[key] = ""
# 2. Özel alanlar için format doğrulama ve dönüşüm
# Yüzde işareti içeren alanları HTML güvenli gösterimi için işaretle
if 'percentage_cpu' in processed_query:
cpu_value = 0.0
try:
# Tür ve format düzeltme
if isinstance(processed_query['percentage_cpu'], str):
# Eğer zaten string ise, sayısal değeri çıkar
if '%' in processed_query['percentage_cpu']:
cpu_value = float(processed_query['percentage_cpu'].replace('%', '').strip())
else:
cpu_value = float(processed_query['percentage_cpu'])
else:
# Sayısal değer ise direkt dönüştür
cpu_value = float(processed_query['percentage_cpu'])
# Grafik için sayısal değeri kaydet
chart_data["cpu_percentages"].append(cpu_value)
# Görüntüleme için güvenli string değere çevir - f-string yerine format() kullan
processed_query['percentage_cpu'] = "{:.2f}%".format(cpu_value)
except (ValueError, TypeError):
chart_data["cpu_percentages"].append(0.0)
processed_query['percentage_cpu'] = "0.00%"
# Toplam çalışma süresi - sayısal değer çıkar
if 'total_time' in processed_query:
time_value = 0.0
try:
if isinstance(processed_query['total_time'], str):
# "XX.XX ms" formatından sayısal değeri çıkar
if 'ms' in processed_query['total_time']:
time_value = float(processed_query['total_time'].replace('ms', '').strip())
else:
time_value = float(processed_query['total_time'])
else:
time_value = float(processed_query['total_time'])
chart_data["time_values"].append(time_value)
except (ValueError, TypeError):
chart_data["time_values"].append(0.0)
else:
chart_data["time_values"].append(0.0)
# Çağrı sayısı
if 'calls' in processed_query:
try:
calls = int(processed_query['calls'])
chart_data["calls"].append(calls)
except (ValueError, TypeError):
chart_data["calls"].append(0)
else:
chart_data["calls"].append(0)
# Sorgu metni/etiketi
if 'query' in processed_query and processed_query['query']:
# Sorgu metnini kısalt, grafik için etiket olarak kullan
query_text = processed_query['query']
chart_label = query_text[:20] + "..." if len(query_text) > 20 else query_text
chart_data["labels"].append(chart_label)
else:
chart_data["labels"].append("Unknown")
processed_queries.append(processed_query)
# Chart.js için veri hazırla - JSON olarak gönderebilmek için güvenli hale getir
# Format string hatalarını önlemek için değerleri string yerine sadece sayısal değerler olarak gönder
try:
# Tamamen yeni bir veri yapısı oluştur
import json
# Yüzde işaretini içeren metinsel değerler yerine sayısal değerler kullan
clean_chart_data = {
"labels": [str(i) for i in range(len(chart_data["labels"]))], # Sadece indis numaralarını gönder
"cpu_percentages": chart_data["cpu_percentages"],
"time_values": chart_data["time_values"],
"calls": chart_data["calls"],
"original_labels": chart_data["labels"] # Orijinal etiketleri farklı bir alanda gönder
}
# Güvenli JSON'a dönüştür - hata yönetimi ekle
try:
chart_data_json = json.dumps(clean_chart_data)
except Exception as e:
app.logger.error(f"JSON serileştirme hatası: {e}")
# Hata durumunda boş veri yapısı
chart_data_json = "{}"
# İşlenmiş verileri template'e aktar
return render_template('performance/queries.html',
db_conn=db_conn,
queries=processed_queries,
summary=summary,
pg_version=pg_version,
mysql_version=mysql_version,
order_by=order_by,
limit=limit,
interval=interval,
chart_data=chart_data_json)
except Exception as json_error:
app.logger.error(f"JSON serileştirmede hata: {json_error}")
# JSON dönüşümünde hata varsa, grafiksiz şablonu göster
return render_template('performance/queries.html',
db_conn=db_conn,
queries=processed_queries,
summary=summary,
pg_version=pg_version,
mysql_version=mysql_version,
order_by=order_by,
limit=limit,
interval=interval,
chart_data="{}")
except Exception as format_error:
# Şablon oluşturulurken bir format hatası olursa daha açıklayıcı bir hata mesajı göster
app.logger.error(f"Şablon oluşturulurken format hatası: {format_error}")
flash('Performans verileri işlenirken bir format hatası oluştu. Veriler basitleştirilmiş formatta gösterilecek.', 'warning')
# Daha güvenli bir şekilde f-string kullan - süslü parantezleri escape et
escaped_name = db_conn.name.replace('{', '{{').replace('}', '}}')
safe_total_calls = str(summary.get('total_calls', 'N/A')).replace('{', '{{').replace('}', '}}')
safe_query_count = str(summary.get('query_count', 'N/A')).replace('{', '{{').replace('}', '}}')
safe_total_time = str(summary.get('total_time', 'N/A')).replace('{', '{{').replace('}', '}}')
# Şablon hatası durumunda basitleştirilmiş bir görünüm kullanarak verileri göster
# f-string yerine format() metodunu kullan
simple_view = """
<div class="container my-4">
<h1>Sorgu Performansı: {escaped_name}</h1>
<div class="alert alert-warning">
<strong>Uyarı:</strong> Performans verileri işlenirken bir format hatası oluştu.
Veriler basitleştirilmiş formatta görüntüleniyor.
</div>
<div class="card mb-4">
<div class="card-header">Özet Bilgiler</div>
<div class="card-body">
<p>Toplam Çağrı: {safe_total_calls}</p>
<p>Sorgu Sayısı: {safe_query_count}</p>
<p>Toplam Çalışma Süresi: {safe_total_time}</p>
</div>
</div>
<a href="{grafik_link}" class="btn btn-success">Grafik Görünümüne Geç</a>
<a href="{back_link}" class="btn btn-secondary">Geri Dön</a>
</div>
""".format(
escaped_name=escaped_name,
safe_total_calls=safe_total_calls,
safe_query_count=safe_query_count,
safe_total_time=safe_total_time,
back_link=url_for('performance_dashboard'),
grafik_link=url_for('performance_charts', db_id=db_conn.id)
)
return simple_view
except Exception as e:
app.logger.error(f"Performans verileri alınırken kritik hata: {str(e)}")
flash(f'Performans verileri alınırken hata: {str(e)}', 'danger')
return redirect(url_for('performance_dashboard'))
@app.route('/performance/database/<int:db_id>')
@login_required
def performance_database(db_id):
"""Veritabanı istatistiklerini göster."""
try:
# Veritabanı bağlantısını al
db_conn = DatabaseConnection.query.get_or_404(db_id)
# Veritabanı bilgilerini hazırla
db_info = {
'host': db_conn.host,
'port': db_conn.port,
'username': db_conn.username,
'password': db_conn.password,
'database': db_conn.database
}
# Veritabanı tipine göre uygun fonksiyonu çağır
if db_conn.db_type == 'postgresql':
# Sabit formatlama kullanan database_performance_fixed modülünü kullan
from database_performance_fixed import get_database_stats as pg_get_database_stats
result = pg_get_database_stats(db_info)
elif db_conn.db_type in ['mysql', 'mariadb'] and MYSQL_PERFORMANCE_AVAILABLE:
# mysql_performance modülünden import edilmiş fonksiyonları kullan
from mysql_performance import get_database_stats as mysql_get_database_stats
result = mysql_get_database_stats(db_info)
else:
flash(_('Veritabanı istatistikleri bu veritabanı türü için desteklenmiyor.'), 'warning')
return redirect(url_for('performance_dashboard'))
# Hata durumunda bilgi mesajı göster
if not result.get('success', False):
flash(result.get('message', 'Bilinmeyen bir hata oluştu'), 'danger')
return redirect(url_for('performance_dashboard'))
# Veri varsa sonuçları render et
return render_template('performance/database.html',
db_conn=db_conn,
stats=result.get('data', {}),
is_mysql=(db_conn.db_type in ['mysql', 'mariadb']))
except Exception as e:
app.logger.error(f"Veritabanı istatistikleri alınırken hata: {str(e)}")
flash(f'Veritabanı istatistikleri alınırken hata: {str(e)}', 'danger')
return redirect(url_for('performance_dashboard'))
@app.route('/performance/charts/<int:db_id>')
@login_required
def performance_charts(db_id):
"""Sorgu performans grafiklerini göster."""
try:
# Veritabanı bağlantısını al
db_conn = DatabaseConnection.query.get_or_404(db_id)
# Sorgu parametrelerini al
limit = int(request.args.get('limit', 10))
order_by = request.args.get('order_by', 'total_exec_time')
# Veritabanı tipine göre uygun modülü seç
try:
# Zaman damgası oluştur - yeni grafikleri eski grafiklerden ayırt etmek için
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
# Veritabanı tipine göre uygun modülü seç ve grafikleri oluştur
if db_conn.db_type == 'postgresql':
import performance_charts
result = performance_charts.main(db_id, limit, order_by)
elif db_conn.db_type in ['mysql', 'mariadb']:
import mysql_performance_charts
result = mysql_performance_charts.main(db_id, limit, order_by)
else:
flash('Bu veritabanı tipi için grafik görünümü desteklenmiyor.', 'warning')
return redirect(url_for('performance_queries', db_id=db_id))
if not result or not result.get('charts'):
if db_conn.db_type == 'postgresql':
flash('Grafikler oluşturulamadı. Lütfen pg_stat_statements modülünün etkin olduğundan emin olun.', 'warning')
elif db_conn.db_type in ['mysql', 'mariadb']:
flash('Grafikler oluşturulamadı. Lütfen performance_schema özelliğinin etkin olduğundan emin olun.', 'warning')
else:
flash('Grafikler oluşturulamadı.', 'warning')
return redirect(url_for('performance_queries', db_id=db_id))
# Grafik yolları ve özet verileri
chart_files = result['charts']
summary = result['summary']
query_count = result['query_count']
# Statik URL'lere dönüştür
chart_urls = {}
for key, path in chart_files.items():
if os.path.exists(path):
# URL yolu olarak dönüştür (/static/... şeklinde)
url = '/' + os.path.relpath(path)
chart_urls[key] = url
# Şablonu render et
return render_template(
'performance/query_charts.html',
db_conn=db_conn,
chart_urls=chart_urls,
summary=summary,
query_count=query_count,
limit=limit,
order_by=order_by,
timestamp=timestamp
)
except ImportError as ie:
app.logger.error(f"Pandas veya Matplotlib kütüphaneleri eksik: {str(ie)}")
flash('Grafik oluşturma için gerekli kütüphaneler eksik.', 'danger')
return redirect(url_for('performance_queries', db_id=db_id))
except Exception as e:
app.logger.error(f"Grafik sayfası yüklenirken hata: {str(e)}")
flash(f'Grafik sayfası yüklenirken hata: {str(e)}', 'danger')
return redirect(url_for('performance_dashboard'))
@app.route('/performance/table/<int:db_id>/<table_name>')
@login_required
def performance_table(db_id, table_name):
"""Tablo istatistiklerini göster."""
try:
# Veritabanı bağlantısını al
db_conn = DatabaseConnection.query.get_or_404(db_id)
# Veritabanı bilgilerini hazırla
db_info = {
'host': db_conn.host,
'port': db_conn.port,
'username': db_conn.username,
'password': db_conn.password,
'database': db_conn.database
}
# Veritabanı tipine göre uygun fonksiyonu çağır
if db_conn.db_type == 'postgresql':
# Sabit formatlama kullanan database_performance_fixed modülünü kullan
from database_performance_fixed import get_table_stats as pg_get_table_stats
result = pg_get_table_stats(db_info, table_name)
elif db_conn.db_type in ['mysql', 'mariadb'] and MYSQL_PERFORMANCE_AVAILABLE:
# mysql_performance modülünden import edilmiş fonksiyonları kullan
from mysql_performance import get_table_stats as mysql_get_table_stats
result = mysql_get_table_stats(db_info, table_name)
else:
flash(_('Tablo istatistikleri bu veritabanı türü için desteklenmiyor.'), 'warning')
return redirect(url_for('performance_dashboard'))
# Hata durumunda bilgi mesajı göster
if not result.get('success', False):
flash(result.get('message', 'Bilinmeyen bir hata oluştu'), 'danger')
return redirect(url_for('performance_database', db_id=db_id))
# Veri varsa sonuçları render et
return render_template('performance/table.html',
db_conn=db_conn,
table_name=table_name,
stats=result.get('data', {}),
is_mysql=(db_conn.db_type in ['mysql', 'mariadb']))
except Exception as e:
app.logger.error(f"Tablo istatistikleri alınırken hata: {str(e)}")
flash(f'Tablo istatistikleri alınırken hata: {str(e)}', 'danger')
return redirect(url_for('performance_database', db_id=db_id))
@app.route('/performance/enable_pgss/<int:db_id>', methods=['POST'])
@login_required
def enable_pgss(db_id):
"""PostgreSQL pg_stat_statements modülünü etkinleştir."""
# Admin yetkisi kontrolü
if not current_user.can_manage_db_connections():
flash(_('Veritabanı modüllerini etkinleştirme izniniz yok.'), 'danger')
return redirect(url_for('performance_dashboard'))
try:
# Veritabanı bağlantısını al
db_conn = DatabaseConnection.query.get_or_404(db_id)
# Sadece PostgreSQL'i destekle
if db_conn.db_type != 'postgresql':
flash(_('pg_stat_statements sadece PostgreSQL için desteklenir.'), 'warning')
return redirect(url_for('performance_dashboard'))
# Modülü etkinleştir
conn_str = "host={} port={} user={} password={} dbname={}".format(db_conn.host, db_conn.port, db_conn.username, db_conn.password, db_conn.database)
connection = psycopg2.connect(conn_str)
from database_performance_fixed import enable_pg_stat_statements
result = enable_pg_stat_statements(connection)
# Sonucu göster
if result.get('success', False):
flash(result.get('message', 'Modül başarıyla etkinleştirildi.'), 'success')
else:
flash(result.get('message', 'Modül etkinleştirilemedi.'), 'danger')
except Exception as e:
flash('pg_stat_statements etkinleştirilirken hata: {}'.format(str(e)), 'danger')
return redirect(url_for('performance_queries', db_id=db_id))
@app.route('/performance/enable_performance_schema/<int:db_id>', methods=['POST'])
@login_required
def enable_performance_schema_route(db_id):
"""MySQL Performance Schema'yı etkinleştir."""
# Admin yetkisi kontrolü
if not current_user.can_manage_db_connections():
flash(_('Veritabanı modüllerini etkinleştirme izniniz yok.'), 'danger')
return redirect(url_for('performance_dashboard'))
# MySQL Performance desteği mevcut mu kontrol et
if not MYSQL_PERFORMANCE_AVAILABLE:
flash(_('MySQL performans modülleri kullanılamıyor.'), 'warning')
return redirect(url_for('performance_dashboard'))
try:
# Veritabanı bağlantısını al
db_conn = DatabaseConnection.query.get_or_404(db_id)
# Sadece MySQL/MariaDB'yi destekle
if db_conn.db_type not in ['mysql', 'mariadb']:
flash(_('Performance Schema sadece MySQL ve MariaDB için desteklenir.'), 'warning')
return redirect(url_for('performance_dashboard'))
# Veritabanı bilgilerini hazırla
db_info = {
'host': db_conn.host,
'port': db_conn.port,
'username': db_conn.username,
'password': db_conn.password,
'database': db_conn.database
}
# Modülü etkinleştir
from mysql_performance import enable_performance_schema
result = enable_performance_schema(db_info)
# Sonucu göster
if result.get('success', False):
flash(result.get('message', 'Performance Schema başarıyla etkinleştirildi.'), 'success')
else:
flash(result.get('message', 'Performance Schema etkinleştirilemedi.'), 'danger')
except Exception as e:
flash('Performance Schema etkinleştirilirken hata: {}'.format(str(e)), 'danger')
return redirect(url_for('performance_queries', db_id=db_id))
@app.route('/performance/reset_stats/<int:db_id>', methods=['POST'])
@login_required
def reset_stats(db_id):
"""Veritabanı sorgu istatistiklerini sıfırla."""
# Admin yetkisi kontrolü
if not current_user.can_manage_db_connections():
flash(_('Sorgu istatistiklerini sıfırlama izniniz yok.'), 'danger')
return redirect(url_for('performance_dashboard'))
try:
# Veritabanı bağlantısını al
db_conn = DatabaseConnection.query.get_or_404(db_id)
# Sorgu parametrelerini al
reset_limit = int(request.args.get('limit', 20))
reset_order_by = request.args.get('order_by', 'total_time')
# Verileri sıfırla
db_info = {
'host': db_conn.host,
'port': db_conn.port,
'username': db_conn.username,
'password': db_conn.password,
'database': db_conn.database
}
# Veritabanı tipine göre uygun fonksiyonu çağır
if db_conn.db_type == 'postgresql':
from database_performance_fixed import get_top_queries as pg_get_top_queries
result = pg_get_top_queries(db_info, reset_limit, reset_order_by, 'reset')
elif db_conn.db_type in ['mysql', 'mariadb'] and MYSQL_PERFORMANCE_AVAILABLE:
from mysql_performance import get_top_queries as mysql_get_top_queries
result = mysql_get_top_queries(db_info, reset_limit, reset_order_by, 'reset')
else:
flash(_('Sorgu istatistikleri sıfırlama bu veritabanı türü için desteklenmiyor.'), 'warning')
return redirect(url_for('performance_dashboard'))
# Sonucu göster
if result.get('success', False):
flash(_('Sorgu istatistikleri başarıyla sıfırlandı.'), 'success')
else:
flash(result.get('message', 'İstatistikler sıfırlanamadı.'), 'danger')
except Exception as e:
flash('Sorgu istatistikleri sıfırlanırken hata: {}'.format(str(e)), 'danger')
reset_limit = int(request.args.get('limit', 20))
reset_order_by = request.args.get('order_by', 'total_time')
return redirect(url_for('performance_queries', db_id=db_id, limit=reset_limit, order_by=reset_order_by))