first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:20:45 +03:00
commit d50f14bcb1
681 changed files with 65020 additions and 0 deletions

0
backup/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

162
backup/admin.py Normal file
View File

@@ -0,0 +1,162 @@
from django.contrib import admin
from django.utils.html import format_html
from django.contrib import messages
from django.utils import timezone
from .models import DatabaseBackup
from .views import BackupManager
@admin.register(DatabaseBackup)
class DatabaseBackupAdmin(admin.ModelAdmin):
list_display = ['name', 'status_badge', 'backup_type', 'file_size_display', 'created_by', 'created_at', 'completed_at']
list_filter = ['status', 'backup_type', 'created_at']
search_fields = ['name', 'notes', 'error_message']
readonly_fields = ['file_path', 'file_size', 'status', 'created_by', 'created_at', 'completed_at', 'error_message', 'file_size_display_field']
fieldsets = (
('Temel Bilgiler', {
'fields': ('name', 'backup_type', 'status', 'notes')
}),
('Yedek Dosya Bilgileri', {
'fields': ('file_path', 'file_size_display_field')
}),
('Zaman Bilgileri', {
'fields': ('created_by', 'created_at', 'completed_at')
}),
('Hata Bilgileri', {
'fields': ('error_message',),
'classes': ('collapse',)
}),
)
actions = ['create_new_backup', 'restore_selected_backup', 'delete_backup_files']
def status_badge(self, obj):
"""Durum için renkli badge gösterir"""
colors = {
'pending': '#FFA500',
'in_progress': '#2196F3',
'completed': '#4CAF50',
'failed': '#F44336',
}
color = colors.get(obj.status, '#999')
return format_html(
'<span style="background-color: {}; color: white; padding: 3px 10px; border-radius: 3px; font-weight: bold;">{}</span>',
color,
obj.get_status_display()
)
status_badge.short_description = 'Durum'
def file_size_display(self, obj):
"""Dosya boyutunu gösterir"""
return obj.get_file_size_display()
file_size_display.short_description = 'Dosya Boyutu'
def file_size_display_field(self, obj):
"""Read-only field için dosya boyutu"""
return obj.get_file_size_display()
file_size_display_field.short_description = 'Dosya Boyutu'
def save_model(self, request, obj, form, change):
"""Model kaydedilirken created_by alanını otomatik doldur"""
if not change: # Yeni kayıt
obj.created_by = request.user
super().save_model(request, obj, form, change)
def create_new_backup(self, request, queryset):
"""Yeni bir yedek oluşturur"""
# Yeni bir backup objesi oluştur
timestamp = timezone.now().strftime('%Y-%m-%d %H:%M:%S')
backup = DatabaseBackup.objects.create(
name=f"Manuel Yedek - {timestamp}",
backup_type='manual',
created_by=request.user,
status='pending'
)
# Yedekleme işlemini başlat
manager = BackupManager()
success, message = manager.create_backup(backup)
if success:
self.message_user(request, message, messages.SUCCESS)
else:
self.message_user(request, message, messages.ERROR)
create_new_backup.short_description = "Yeni Yedek Oluştur"
def restore_selected_backup(self, request, queryset):
"""Seçili yedeği geri yükler"""
if queryset.count() != 1:
self.message_user(
request,
"Lütfen geri yüklemek için sadece bir yedek seçin",
messages.WARNING
)
return
backup = queryset.first()
if backup.status != 'completed':
self.message_user(
request,
"Sadece tamamlanmış yedekler geri yüklenebilir",
messages.WARNING
)
return
if not backup.file_path:
self.message_user(
request,
"Yedek dosya yolu bulunamadı",
messages.ERROR
)
return
manager = BackupManager()
success, message = manager.restore_backup(backup.file_path)
if success:
self.message_user(request, message, messages.SUCCESS)
else:
self.message_user(request, message, messages.ERROR)
restore_selected_backup.short_description = "Seçili Yedeği Geri Yükle"
def delete_backup_files(self, request, queryset):
"""Seçili yedeklerin dosyalarını siler"""
deleted_count = 0
error_count = 0
manager = BackupManager()
for backup in queryset:
if backup.file_path:
success, message = manager.delete_backup_file(backup.file_path)
if success:
backup.file_path = None
backup.file_size = None
backup.save()
deleted_count += 1
else:
error_count += 1
if deleted_count > 0:
self.message_user(
request,
f"{deleted_count} yedek dosyası silindi",
messages.SUCCESS
)
if error_count > 0:
self.message_user(
request,
f"{error_count} yedek dosyası silinemedi",
messages.WARNING
)
delete_backup_files.short_description = "Yedek Dosyalarını Sil"
def has_delete_permission(self, request, obj=None):
"""Silme iznini kontrol et - Tüm admin kullanıcıları silebilir"""
return request.user.is_staff

5
backup/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class BackupConfig(AppConfig):
name = 'backup'

View File

@@ -0,0 +1,38 @@
# Generated by Django 6.0 on 2025-12-22 16:52
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='DatabaseBackup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Yedek Adı')),
('file_path', models.CharField(blank=True, max_length=500, null=True, verbose_name='Dosya Yolu')),
('file_size', models.BigIntegerField(blank=True, null=True, verbose_name='Dosya Boyutu (bytes)')),
('status', models.CharField(choices=[('pending', 'Bekliyor'), ('in_progress', 'İşleniyor'), ('completed', 'Tamamlandı'), ('failed', 'Başarısız')], default='pending', max_length=20, verbose_name='Durum')),
('backup_type', models.CharField(choices=[('manual', 'Manuel'), ('automatic', 'Otomatik')], default='manual', max_length=20, verbose_name='Yedek Tipi')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Oluşturulma Tarihi')),
('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='Tamamlanma Tarihi')),
('error_message', models.TextField(blank=True, null=True, verbose_name='Hata Mesajı')),
('notes', models.TextField(blank=True, null=True, verbose_name='Notlar')),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Oluşturan')),
],
options={
'verbose_name': 'Veritabanı Yedeği',
'verbose_name_plural': 'Veritabanı Yedekleri',
'ordering': ['-created_at'],
},
),
]

View File

Binary file not shown.

51
backup/models.py Normal file
View File

@@ -0,0 +1,51 @@
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class DatabaseBackup(models.Model):
"""Veritabanı yedekleme kayıtlarını tutar"""
STATUS_CHOICES = [
('pending', 'Bekliyor'),
('in_progress', 'İşleniyor'),
('completed', 'Tamamlandı'),
('failed', 'Başarısız'),
]
BACKUP_TYPE_CHOICES = [
('manual', 'Manuel'),
('automatic', 'Otomatik'),
]
name = models.CharField(max_length=255, verbose_name='Yedek Adı')
file_path = models.CharField(max_length=500, verbose_name='Dosya Yolu', blank=True, null=True)
file_size = models.BigIntegerField(verbose_name='Dosya Boyutu (bytes)', null=True, blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name='Durum')
backup_type = models.CharField(max_length=20, choices=BACKUP_TYPE_CHOICES, default='manual', verbose_name='Yedek Tipi')
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='Oluşturan')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Oluşturulma Tarihi')
completed_at = models.DateTimeField(null=True, blank=True, verbose_name='Tamamlanma Tarihi')
error_message = models.TextField(blank=True, null=True, verbose_name='Hata Mesajı')
notes = models.TextField(blank=True, null=True, verbose_name='Notlar')
class Meta:
verbose_name = 'Veritabanı Yedeği'
verbose_name_plural = 'Veritabanı Yedekleri'
ordering = ['-created_at']
def __str__(self):
return f"{self.name} - {self.get_status_display()}"
def get_file_size_display(self):
"""Dosya boyutunu okunabilir formatta döndürür"""
if not self.file_size:
return "N/A"
size = self.file_size
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024.0:
return f"{size:.2f} {unit}"
size /= 1024.0
return f"{size:.2f} TB"

3
backup/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

259
backup/views.py Normal file
View File

@@ -0,0 +1,259 @@
import os
from datetime import datetime
from django.conf import settings
from django.utils import timezone
from .models import DatabaseBackup
from django.tasks import task
try:
import psycopg2
from psycopg2 import sql
PSYCOPG2_AVAILABLE = True
except ImportError:
PSYCOPG2_AVAILABLE = False
class BackupManager:
"""PostgreSQL veritabanı yedekleme işlemlerini yönetir - Sadece psycopg2 kullanarak"""
def __init__(self):
self.backup_dir = os.path.join(settings.BASE_DIR, 'backups')
if not os.path.exists(self.backup_dir):
os.makedirs(self.backup_dir)
def get_db_config(self):
"""Veritabanı yapılandırmasını alır"""
db_config = settings.DATABASES['default']
return {
'dbname': db_config.get('NAME'),
'user': db_config.get('USER'),
'password': db_config.get('PASSWORD'),
'host': db_config.get('HOST', 'localhost'),
'port': db_config.get('PORT', '5432'),
}
def get_connection(self):
"""PostgreSQL bağlantısı oluşturur"""
if not PSYCOPG2_AVAILABLE:
raise Exception("psycopg2 kütüphanesi yüklü değil")
db_config = self.get_db_config()
return psycopg2.connect(
dbname=db_config['dbname'],
user=db_config['user'],
password=db_config['password'],
host=db_config['host'],
port=db_config['port']
)
#@task
def create_backup(self, backup_obj):
"""
PostgreSQL veritabanının yedeğini oluşturur
Sadece psycopg2 kullanarak SQL dump oluşturur
"""
try:
backup_obj.status = 'in_progress'
backup_obj.save()
db_config = self.get_db_config()
# Yedek dosyası adını oluştur
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_filename = f"backup_{db_config['dbname']}_{timestamp}.sql"
backup_path = os.path.join(self.backup_dir, backup_filename)
# Veritabanına bağlan
conn = self.get_connection()
cursor = conn.cursor()
with open(backup_path, 'w', encoding='utf-8') as f:
# Header
f.write("-- PostgreSQL Database Backup\n")
f.write(f"-- Database: {db_config['dbname']}\n")
f.write(f"-- Date: {datetime.now()}\n")
f.write("-- Created by Django Backup System using psycopg2\n\n")
f.write("SET client_encoding = 'UTF8';\n")
f.write("SET standard_conforming_strings = on;\n")
f.write("SET check_function_bodies = false;\n")
f.write("SET client_min_messages = warning;\n\n")
# Tüm tabloları al
cursor.execute("""
SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
""")
tables = cursor.fetchall()
for (table_name,) in tables:
f.write(f"\n-- Table: {table_name}\n")
# Tablo yapısını al - kolon bilgilerini çek
cursor.execute("""
SELECT
column_name,
data_type,
character_maximum_length,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = %s
ORDER BY ordinal_position;
""", [table_name])
columns_info = cursor.fetchall()
if columns_info:
f.write(f"DROP TABLE IF EXISTS {table_name} CASCADE;\n")
f.write(f"CREATE TABLE {table_name} (\n")
col_defs = []
for col_name, data_type, max_length, is_nullable, col_default in columns_info:
col_def = f" {col_name} "
# Veri tipini ekle
if max_length and data_type == 'character varying':
col_def += f"VARCHAR({max_length})"
elif max_length and data_type == 'character':
col_def += f"CHAR({max_length})"
else:
col_def += data_type.upper()
# NOT NULL
if is_nullable == 'NO':
col_def += " NOT NULL"
# DEFAULT değer
if col_default:
col_def += f" DEFAULT {col_default}"
col_defs.append(col_def)
f.write(",\n".join(col_defs))
f.write("\n);\n\n")
# Veriyi al ve INSERT komutları oluştur
cursor.execute(sql.SQL("SELECT * FROM {}").format(sql.Identifier(table_name)))
rows = cursor.fetchall()
if rows:
# Kolon isimlerini al
cursor.execute(sql.SQL("""
SELECT column_name
FROM information_schema.columns
WHERE table_name = %s
ORDER BY ordinal_position;
"""), [table_name])
columns = [row[0] for row in cursor.fetchall()]
f.write(f"-- Data for table: {table_name}\n")
for row in rows:
values = []
for val in row:
if val is None:
values.append('NULL')
elif isinstance(val, str):
# SQL injection'a karşı koruma
escaped = val.replace("'", "''")
values.append(f"'{escaped}'")
elif isinstance(val, (int, float)):
values.append(str(val))
elif isinstance(val, bool):
values.append('TRUE' if val else 'FALSE')
else:
# Diğer tipler için string'e çevir
escaped = str(val).replace("'", "''")
values.append(f"'{escaped}'")
cols_str = ', '.join(columns)
vals_str = ', '.join(values)
f.write(f"INSERT INTO {table_name} ({cols_str}) VALUES ({vals_str});\n")
f.write("\n")
# Sequence'leri sıfırla
f.write("\n-- Reset sequences\n")
cursor.execute("""
SELECT
c.relname as sequence_name,
t.relname as table_name,
a.attname as column_name
FROM pg_class c
JOIN pg_depend d ON d.objid = c.oid
JOIN pg_class t ON d.refobjid = t.oid
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.refobjsubid
WHERE c.relkind = 'S'
AND t.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public');
""")
sequences = cursor.fetchall()
for seq_name, tbl_name, col_name in sequences:
f.write(f"SELECT setval('{seq_name}', (SELECT COALESCE(MAX({col_name}), 1) FROM {tbl_name}));\n")
cursor.close()
conn.close()
# Başarılı
file_size = os.path.getsize(backup_path)
backup_obj.file_path = backup_path
backup_obj.file_size = file_size
backup_obj.status = 'completed'
backup_obj.completed_at = timezone.now()
backup_obj.save()
return True, f"Yedekleme başarıyla tamamlandı: {backup_filename}"
except Exception as e:
backup_obj.status = 'failed'
backup_obj.error_message = str(e)
backup_obj.save()
return False, f"Yedekleme hatası: {str(e)}"
def restore_backup(self, backup_path):
"""
Bir yedek dosyasından veritabanını geri yükler
SQL dosyasını okuyup komutları çalıştırır
"""
try:
if not os.path.exists(backup_path):
return False, "Yedek dosyası bulunamadı"
# SQL dosyasını oku
with open(backup_path, 'r', encoding='utf-8') as f:
sql_content = f.read()
# Veritabanına bağlan
conn = self.get_connection()
conn.autocommit = True
cursor = conn.cursor()
# SQL komutlarını çalıştır
# Basit bir şekilde satır satır çalıştır
statements = sql_content.split(';')
for statement in statements:
statement = statement.strip()
if statement and not statement.startswith('--'):
try:
cursor.execute(statement)
except Exception as e:
# Bazı hatalar göz ardı edilebilir (örn: tablo zaten var)
if 'does not exist' not in str(e):
print(f"Warning: {e}")
cursor.close()
conn.close()
return True, "Veritabanı başarıyla geri yüklendi"
except Exception as e:
return False, f"Geri yükleme hatası: {str(e)}"
def delete_backup_file(self, backup_path):
"""Yedek dosyasını fiziksel olarak siler"""
try:
if os.path.exists(backup_path):
os.remove(backup_path)
return True, "Yedek dosyası silindi"
else:
return False, "Yedek dosyası bulunamadı"
except Exception as e:
return False, f"Dosya silme hatası: {str(e)}"