first commit
This commit is contained in:
2
backup/__init__.py
Normal file
2
backup/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
default_app_config = 'backup.apps.BackupConfig'
|
||||
|
||||
368
backup/admin.py
Normal file
368
backup/admin.py
Normal file
@@ -0,0 +1,368 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.contrib import messages
|
||||
from django.utils import timezone
|
||||
from django.http import FileResponse, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, render, redirect
|
||||
from django.urls import path, reverse
|
||||
from django.conf import settings
|
||||
import os
|
||||
from datetime import datetime
|
||||
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', 'download_link', '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', 'download_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 download_link(self, obj):
|
||||
"""İndir butonu gösterir"""
|
||||
if obj.file_path and obj.status == 'completed' and os.path.isfile(obj.file_path):
|
||||
url = f'/admin/backup/databasebackup/{obj.pk}/download/'
|
||||
return format_html(
|
||||
'<a href="{}" class="button" style="background-color: #4CAF50; color: white; padding: 5px 10px; '
|
||||
'text-decoration: none; border-radius: 3px; display: inline-block;">📥 İndir</a>',
|
||||
url
|
||||
)
|
||||
return format_html('<span style="color: {};">-</span>', '#999')
|
||||
download_link.short_description = 'İndir'
|
||||
|
||||
def get_urls(self):
|
||||
"""Admin için özel URL'ler ekler"""
|
||||
urls = super().get_urls()
|
||||
custom_urls = [
|
||||
path('create-backup/', self.admin_site.admin_view(self.create_backup_view), name='backup_create'),
|
||||
path('upload-backup/', self.admin_site.admin_view(self.upload_backup_view), name='backup_upload'),
|
||||
path('<int:backup_id>/download/', self.admin_site.admin_view(self.download_backup_file), name='backup_download'),
|
||||
]
|
||||
return custom_urls + urls
|
||||
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
"""Change list view'a ekstra context ekler"""
|
||||
extra_context = extra_context or {}
|
||||
extra_context['show_create_backup_button'] = True
|
||||
extra_context['show_upload_backup_button'] = True
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
def create_backup_view(self, request):
|
||||
"""Yeni yedek oluşturma view'i"""
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
|
||||
# Yeni bir backup objesi oluştur
|
||||
timestamp = timezone.now().strftime('%Y%m%d_%H%M%S')
|
||||
backup = DatabaseBackup.objects.create(
|
||||
name=f"Manuel Yedek - {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||
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)
|
||||
|
||||
# Liste sayfasına yönlendir
|
||||
return redirect(reverse('admin:backup_databasebackup_changelist'))
|
||||
|
||||
def upload_backup_view(self, request):
|
||||
"""Yedek dosyası yükleme view'i"""
|
||||
if request.method == 'POST':
|
||||
uploaded_file = request.FILES.get('backup_file')
|
||||
backup_name = request.POST.get('backup_name', '')
|
||||
|
||||
if not uploaded_file:
|
||||
self.message_user(request, "Lütfen bir dosya seçin", messages.ERROR)
|
||||
return redirect(reverse('admin:backup_upload'))
|
||||
|
||||
# Dosya uzantısı kontrolü
|
||||
if not uploaded_file.name.endswith('.sql'):
|
||||
self.message_user(request, "Sadece .sql uzantılı dosyalar yüklenebilir", messages.ERROR)
|
||||
return redirect(reverse('admin:backup_upload'))
|
||||
|
||||
# Dosya boyutu kontrolü (max 500MB)
|
||||
max_size = 500 * 1024 * 1024 # 500MB in bytes
|
||||
if uploaded_file.size > max_size:
|
||||
self.message_user(request, "Dosya çok büyük. Maksimum 500MB olabilir", messages.ERROR)
|
||||
return redirect(reverse('admin:backup_upload'))
|
||||
|
||||
try:
|
||||
# Backup klasörünü kontrol et
|
||||
manager = BackupManager()
|
||||
backup_dir = manager.backup_dir
|
||||
|
||||
# Dosya adını oluştur (timestamp ekle)
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
original_name = os.path.splitext(uploaded_file.name)[0]
|
||||
filename = f"uploaded_{original_name}_{timestamp}.sql"
|
||||
file_path = os.path.join(backup_dir, filename)
|
||||
|
||||
# Dosyayı kaydet
|
||||
with open(file_path, 'wb+') as destination:
|
||||
for chunk in uploaded_file.chunks():
|
||||
destination.write(chunk)
|
||||
|
||||
# Veritabanı kaydı oluştur
|
||||
if not backup_name:
|
||||
backup_name = f"Yüklenen Yedek - {uploaded_file.name}"
|
||||
|
||||
backup = DatabaseBackup.objects.create(
|
||||
name=backup_name,
|
||||
file_path=file_path,
|
||||
file_size=uploaded_file.size,
|
||||
status='completed',
|
||||
backup_type='manual',
|
||||
created_by=request.user,
|
||||
completed_at=timezone.now(),
|
||||
notes=f"Dosya yüklendi: {uploaded_file.name}"
|
||||
)
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
f"Yedek dosyası başarıyla yüklendi: {uploaded_file.name} ({backup.get_file_size_display()})",
|
||||
messages.SUCCESS
|
||||
)
|
||||
return redirect(reverse('admin:backup_databasebackup_changelist'))
|
||||
|
||||
except Exception as e:
|
||||
self.message_user(request, f"Dosya yüklenirken hata oluştu: {str(e)}", messages.ERROR)
|
||||
return redirect(reverse('admin:backup_upload'))
|
||||
|
||||
# GET request - form göster
|
||||
context = {
|
||||
**self.admin_site.each_context(request),
|
||||
'title': 'Yedek Dosyası Yükle',
|
||||
'opts': self.model._meta,
|
||||
'has_view_permission': self.has_view_permission(request),
|
||||
}
|
||||
return render(request, 'admin/backup/upload_backup.html', context)
|
||||
|
||||
def download_backup_file(self, request, backup_id):
|
||||
"""Yedek dosyasını indirir"""
|
||||
backup = get_object_or_404(DatabaseBackup, pk=backup_id)
|
||||
|
||||
if not backup.file_path:
|
||||
self.message_user(request, "Yedek dosyası bulunamadı", messages.ERROR)
|
||||
return HttpResponse("Dosya bulunamadı", status=404)
|
||||
|
||||
if not os.path.isfile(backup.file_path):
|
||||
self.message_user(request, "Yedek dosyası disk üzerinde bulunamadı", messages.ERROR)
|
||||
return HttpResponse("Dosya disk üzerinde bulunamadı", status=404)
|
||||
|
||||
# Dosyayı indir
|
||||
try:
|
||||
response = FileResponse(open(backup.file_path, 'rb'), content_type='application/sql')
|
||||
filename = os.path.basename(backup.file_path)
|
||||
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||
return response
|
||||
except Exception as e:
|
||||
return HttpResponse(f"Dosya indirilemedi: {str(e)}", status=500)
|
||||
|
||||
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
|
||||
|
||||
# Restore işlemi (migration'lar da dahil)
|
||||
manager = BackupManager()
|
||||
success, message = manager.restore_backup(backup.file_path)
|
||||
|
||||
if success:
|
||||
# Otomatik migration çalıştır
|
||||
try:
|
||||
from django.core.management import call_command
|
||||
import io
|
||||
call_command('migrate', '--noinput', stdout=io.StringIO(), stderr=io.StringIO())
|
||||
self.message_user(request, f"{message} Migration'lar uygulandı. Sayfayı yenileyin.", messages.SUCCESS)
|
||||
except:
|
||||
self.message_user(request, f"{message} Sayfayı yenileyin.", messages.SUCCESS)
|
||||
else:
|
||||
self.message_user(request, message, messages.ERROR)
|
||||
|
||||
restore_selected_backup.short_description = "Seçili Yedeği Geri Yükle"
|
||||
|
||||
def download_backup(self, request, queryset):
|
||||
"""Seçili yedeği indirir"""
|
||||
if queryset.count() != 1:
|
||||
self.message_user(
|
||||
request,
|
||||
"Lütfen indirmek için sadece bir yedek seçin",
|
||||
messages.WARNING
|
||||
)
|
||||
return
|
||||
|
||||
backup = queryset.first()
|
||||
|
||||
if backup.status != 'completed':
|
||||
self.message_user(
|
||||
request,
|
||||
"Sadece tamamlanmış yedekler indirilebilir",
|
||||
messages.WARNING
|
||||
)
|
||||
return
|
||||
|
||||
if not backup.file_path or not os.path.isfile(backup.file_path):
|
||||
self.message_user(
|
||||
request,
|
||||
"Yedek dosyası bulunamadı",
|
||||
messages.ERROR
|
||||
)
|
||||
return
|
||||
|
||||
# Dosyayı indir
|
||||
try:
|
||||
response = FileResponse(open(backup.file_path, 'rb'), content_type='application/sql')
|
||||
filename = os.path.basename(backup.file_path)
|
||||
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||
return response
|
||||
except Exception as e:
|
||||
self.message_user(
|
||||
request,
|
||||
f"Dosya indirilemedi: {str(e)}",
|
||||
messages.ERROR
|
||||
)
|
||||
return
|
||||
|
||||
download_backup.short_description = "Seçili Yedeği İndir"
|
||||
|
||||
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
|
||||
10
backup/apps.py
Normal file
10
backup/apps.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BackupConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'backup'
|
||||
|
||||
def ready(self):
|
||||
"""Uygulama hazır olduğunda sinyalleri import et"""
|
||||
import backup.models # Sinyalleri kaydetmek için import et
|
||||
38
backup/migrations/0001_initial.py
Normal file
38
backup/migrations/0001_initial.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
backup/migrations/__init__.py
Normal file
0
backup/migrations/__init__.py
Normal file
68
backup/models.py
Normal file
68
backup/models.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models.signals import post_delete
|
||||
from django.dispatch import receiver
|
||||
import os
|
||||
|
||||
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"
|
||||
|
||||
|
||||
@receiver(post_delete, sender=DatabaseBackup)
|
||||
def delete_backup_file(sender, instance, **kwargs):
|
||||
"""
|
||||
Backup kaydı silindiğinde, ilişkili fiziksel dosyayı da siler
|
||||
"""
|
||||
if instance.file_path and os.path.isfile(instance.file_path):
|
||||
try:
|
||||
os.remove(instance.file_path)
|
||||
print(f"Yedek dosyası silindi: {instance.file_path}")
|
||||
except Exception as e:
|
||||
print(f"Dosya silinirken hata oluştu: {e}")
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "admin/change_list.html" %}
|
||||
{% load i18n admin_urls static %}
|
||||
|
||||
{% block object-tools-items %}
|
||||
{{ block.super }}
|
||||
{% if show_create_backup_button %}
|
||||
<li>
|
||||
<a href="{% url 'admin:backup_create' %}" class="addlink" style="background-color: #4CAF50; color: white; padding: 10px 15px; text-decoration: none; border-radius: 4px; display: inline-block; font-weight: bold;">
|
||||
🔄 Yeni Yedek Al
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if show_upload_backup_button %}
|
||||
<li>
|
||||
<a href="{% url 'admin:backup_upload' %}" class="addlink" style="background-color: #2196F3; color: white; padding: 10px 15px; text-decoration: none; border-radius: 4px; display: inline-block; font-weight: bold;">
|
||||
📤 Yedek Yükle
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
174
backup/templates/admin/backup/upload_backup.html
Normal file
174
backup/templates/admin/backup/upload_backup.html
Normal file
@@ -0,0 +1,174 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.upload-form-container {
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
}
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="file"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.form-group input[type="file"] {
|
||||
padding: 8px;
|
||||
}
|
||||
.help-text {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
.btn-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: #1976D2;
|
||||
}
|
||||
.btn-secondary {
|
||||
background-color: #757575;
|
||||
color: white;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background-color: #616161;
|
||||
}
|
||||
.info-box {
|
||||
background-color: #E3F2FD;
|
||||
border-left: 4px solid #2196F3;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.info-box h4 {
|
||||
margin-top: 0;
|
||||
color: #1976D2;
|
||||
}
|
||||
.info-box ul {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
|
||||
› <a href="{% url 'admin:backup_databasebackup_changelist' %}">Veritabanı Yedekleri</a>
|
||||
› Yedek Yükle
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="upload-form-container">
|
||||
<h1>📤 Yedek Dosyası Yükle</h1>
|
||||
|
||||
<div class="info-box">
|
||||
<h4>ℹ️ Bilgilendirme</h4>
|
||||
<ul>
|
||||
<li>Sadece <strong>.sql</strong> uzantılı dosyalar yüklenebilir</li>
|
||||
<li>Maksimum dosya boyutu: <strong>500 MB</strong></li>
|
||||
<li>Yüklenen dosya <code>backups/</code> klasörüne kaydedilecektir</li>
|
||||
<li>Dosya otomatik olarak timestamp ile adlandırılacaktır</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="backup_name">Yedek Adı:</label>
|
||||
<input type="text"
|
||||
id="backup_name"
|
||||
name="backup_name"
|
||||
placeholder="Örn: Production Yedek - 2024-12-24"
|
||||
maxlength="255">
|
||||
<span class="help-text">
|
||||
Boş bırakılırsa dosya adı kullanılacaktır
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="backup_file">Yedek Dosyası: *</label>
|
||||
<input type="file"
|
||||
id="backup_file"
|
||||
name="backup_file"
|
||||
accept=".sql"
|
||||
required>
|
||||
<span class="help-text">
|
||||
PostgreSQL SQL dump dosyası (.sql)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="btn-container">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
📤 Yükle
|
||||
</button>
|
||||
<a href="{% url 'admin:backup_databasebackup_changelist' %}" class="btn btn-secondary">
|
||||
❌ İptal
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Dosya seçildiğinde boyut kontrolü
|
||||
document.getElementById('backup_file').addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const maxSize = 500 * 1024 * 1024; // 500MB
|
||||
if (file.size > maxSize) {
|
||||
alert('Dosya çok büyük! Maksimum 500MB olabilir.');
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Dosya adını backup_name alanına otomatik doldur (eğer boşsa)
|
||||
const nameField = document.getElementById('backup_name');
|
||||
if (!nameField.value) {
|
||||
const fileName = file.name.replace('.sql', '');
|
||||
nameField.value = 'Yüklenen Yedek - ' + fileName;
|
||||
}
|
||||
|
||||
// Dosya boyutunu göster
|
||||
const sizeInMB = (file.size / (1024 * 1024)).toFixed(2);
|
||||
console.log('Dosya boyutu: ' + sizeInMB + ' MB');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
3
backup/tests.py
Normal file
3
backup/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
328
backup/views.py
Normal file
328
backup/views.py
Normal file
@@ -0,0 +1,328 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from .models import DatabaseBackup
|
||||
|
||||
|
||||
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,
|
||||
is_identity
|
||||
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, is_identity in columns_info:
|
||||
col_def = f" \"{col_name}\" "
|
||||
|
||||
# Serial kontrolü (Nextval veya Identity)
|
||||
is_serial = False
|
||||
if (col_default and 'nextval' in col_default) or is_identity == 'YES':
|
||||
if data_type == 'integer':
|
||||
col_def += "SERIAL"
|
||||
is_serial = True
|
||||
elif data_type == 'bigint':
|
||||
col_def += "BIGSERIAL"
|
||||
is_serial = True
|
||||
|
||||
if not is_serial:
|
||||
# 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
|
||||
# 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()]
|
||||
|
||||
if not columns:
|
||||
continue
|
||||
|
||||
cursor.execute(sql.SQL("SELECT * FROM {}").format(sql.Identifier(table_name)))
|
||||
rows = cursor.fetchall()
|
||||
|
||||
if rows:
|
||||
f.write(f"-- Data for table: {table_name}\n")
|
||||
|
||||
# INSERT şablonu hazırla
|
||||
cols_str = ', '.join([f'"{c}"' for c in columns]) # Identifier quoting
|
||||
placeholders = ', '.join(['%s'] * len(columns))
|
||||
insert_template = f"INSERT INTO \"{table_name}\" ({cols_str}) VALUES ({placeholders})"
|
||||
|
||||
for row in rows:
|
||||
# mogrify kullanarak güvenli SQL oluştur
|
||||
try:
|
||||
# mogrify bytes döndürür, decode etmemiz lazım
|
||||
safe_sql = cursor.mogrify(insert_template, row).decode('utf-8')
|
||||
f.write(f"{safe_sql};\n")
|
||||
except Exception as row_err:
|
||||
print(f"Row error in {table_name}: {row_err}")
|
||||
continue
|
||||
|
||||
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):
|
||||
"""
|
||||
TAMAMEN OTOMATIK FULL RESTORE
|
||||
Manuel işlem gerektirmez!
|
||||
"""
|
||||
try:
|
||||
if not os.path.exists(backup_path):
|
||||
return False, "Yedek dosyası bulunamadı"
|
||||
|
||||
with open(backup_path, 'r', encoding='utf-8') as f:
|
||||
sql_content = f.read()
|
||||
|
||||
# HOTFIX 1: 'order' gibi keywordlerin tırnak içine alınmaması sorununu düzelt
|
||||
import re
|
||||
sql_content = re.sub(r'(\s+)order(\s+[A-Z]+)', r'\1"order"\2', sql_content)
|
||||
|
||||
# HOTFIX 2: SERIAL/Sequence düzeltmesi
|
||||
# "id INTEGER NOT NULL DEFAULT nextval(...)" -> "id SERIAL"
|
||||
sql_content = re.sub(r'INTEGER\s+NOT\s+NULL\s+DEFAULT\s+nextval\(\'[^\']+\'(:?::regclass)?\)', 'SERIAL', sql_content)
|
||||
sql_content = re.sub(r'BIGINT\s+NOT\s+NULL\s+DEFAULT\s+nextval\(\'[^\']+\'(:?::regclass)?\)', 'BIGSERIAL', sql_content)
|
||||
|
||||
# HOTFIX 3: "id" kolonları INTEGER/BIGINT NOT NULL ise (ve default yoksa) SERIAL yap
|
||||
# Bu durum Identity kolonlarının yanlış yedeklenmesi sonucu oluşur
|
||||
sql_content = re.sub(r'"id"\s+INTEGER\s+NOT\s+NULL(?!(\s+DEFAULT))', '"id" SERIAL', sql_content)
|
||||
sql_content = re.sub(r'"id"\s+BIGINT\s+NOT\s+NULL(?!(\s+DEFAULT))', '"id" BIGSERIAL', sql_content)
|
||||
|
||||
# HOTFIX 4: setval satırlarını kaldır (çünkü biz kendimiz yeniden ayarlıyoruz ve isimler değişmiş olabilir)
|
||||
# Lines starting with SELECT setval...
|
||||
sql_lines = []
|
||||
for line in sql_content.split('\n'):
|
||||
if 'SELECT setval' in line and 'django_migrations' in line or 'SELECT setval' in line:
|
||||
continue # Skip setvals from file
|
||||
sql_lines.append(line)
|
||||
sql_content = '\n'.join(sql_lines)
|
||||
|
||||
conn = self.get_connection()
|
||||
conn.autocommit = True
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
print("=" * 60)
|
||||
print("TAM OTOMATIK RESTORE (YENI VERSIYON)")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. DROP tüm tablolar
|
||||
print("\n1. Temizleniyor...")
|
||||
cursor.execute("SELECT tablename FROM pg_tables WHERE schemaname = 'public';")
|
||||
tables = cursor.fetchall()
|
||||
for (t,) in tables:
|
||||
print(f" Dropping {t}...")
|
||||
cursor.execute(f'DROP TABLE IF EXISTS "{t}" CASCADE;')
|
||||
print(" ✓ Temizlendi")
|
||||
|
||||
# 2. SQL Execution - Tek seferde çalıştır
|
||||
print("\n2. SQL Dosyası Çalıştırılıyor...")
|
||||
# execute() methodu çoklu sorguları çalıştırabilir (psycopg2 özelliği)
|
||||
try:
|
||||
cursor.execute(sql_content)
|
||||
print(" ✓ SQL Script çalıştırıldı")
|
||||
except Exception as sql_err:
|
||||
print(f" SQL HATA: {sql_err}")
|
||||
raise sql_err
|
||||
print(" ✓ SQL Script çalıştırıldı")
|
||||
|
||||
# 3. Sequence'ler (SQL script içinde genelde vardır ama garanti olsun)
|
||||
print("\n3. Sequence'ler Kontrol Ediliyor...")
|
||||
cursor.execute("""
|
||||
SELECT c.relname, t.relname, a.attname
|
||||
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');
|
||||
""")
|
||||
for seq, tbl, col in cursor.fetchall():
|
||||
try:
|
||||
cursor.execute(f"SELECT setval('{seq}', COALESCE((SELECT MAX({col}) FROM {tbl}), 1));")
|
||||
except:
|
||||
pass
|
||||
print(" ✓ Ayarlandı")
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("RESTORE TAMAMLANDI!")
|
||||
print("=" * 60)
|
||||
|
||||
return True, "Restore başarıyla tamamlandı!"
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nHATA: {e}")
|
||||
cursor.close()
|
||||
conn.close()
|
||||
raise
|
||||
|
||||
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)}"
|
||||
Reference in New Issue
Block a user