yeni
This commit is contained in:
@ -1,5 +1,40 @@
|
||||
from django.contrib import admin
|
||||
from .models import SSHCredential, Project, SSHLog
|
||||
from .models import SSHCredential, Project, SSHLog, Customer, Invoice, InvoiceItem
|
||||
from .system_settings import SystemSettings
|
||||
|
||||
@admin.register(SystemSettings)
|
||||
class SystemSettingsAdmin(admin.ModelAdmin):
|
||||
list_display = ('__str__', 'backup_enabled', 'backup_frequency', 'updated_at')
|
||||
fieldsets = (
|
||||
('Yedekleme Ayarları', {
|
||||
'fields': ('backup_enabled', 'backup_frequency', 'backup_hour', 'backup_minute',
|
||||
'backup_day_of_week', 'backup_day_of_month', 'backup_retention_days',
|
||||
'backup_compression', 'backup_format', 'backup_directory')
|
||||
}),
|
||||
('S3 Depolama', {
|
||||
'fields': ('use_s3_storage', 's3_access_key', 's3_secret_key',
|
||||
's3_bucket_name', 's3_region')
|
||||
}),
|
||||
('Bildirim Ayarları', {
|
||||
'fields': ('email_notifications', 'notification_email')
|
||||
}),
|
||||
('SSH Ayarları', {
|
||||
'fields': ('ssh_key_path', 'ssh_key_passphrase')
|
||||
}),
|
||||
('Crontab Bilgisi', {
|
||||
'fields': ('backup_crontab_expression', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
})
|
||||
)
|
||||
readonly_fields = ('backup_crontab_expression', 'updated_at')
|
||||
|
||||
def has_add_permission(self, request):
|
||||
# Sadece bir kayıt olabilir
|
||||
return SystemSettings.objects.count() == 0
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
# Silme işlemi engellenir
|
||||
return False
|
||||
|
||||
@admin.register(SSHCredential)
|
||||
class SSHCredentialAdmin(admin.ModelAdmin):
|
||||
@ -68,4 +103,54 @@ class SSHLogAdmin(admin.ModelAdmin):
|
||||
return False # Log kayıtları manuel olarak eklenemez
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False # Log kayıtları değiştirilemez
|
||||
return False # Log kayıtları değiştirilemez
|
||||
|
||||
class InvoiceItemInline(admin.TabularInline):
|
||||
model = InvoiceItem
|
||||
extra = 1
|
||||
fields = ('project', 'description', 'amount')
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
formset = super().get_formset(request, obj, **kwargs)
|
||||
form = formset.form
|
||||
form.base_fields['description'].widget.attrs['style'] = 'width: 300px'
|
||||
return formset
|
||||
|
||||
|
||||
@admin.register(Invoice)
|
||||
class InvoiceAdmin(admin.ModelAdmin):
|
||||
list_display = ('invoice_number', 'customer', 'issue_date', 'due_date', 'status', 'total_amount')
|
||||
list_filter = ('status', 'issue_date', 'payment_method')
|
||||
search_fields = ('invoice_number', 'customer__name', 'customer__company_name')
|
||||
readonly_fields = ('invoice_number', 'total_amount', 'created_at', 'updated_at')
|
||||
inlines = [InvoiceItemInline]
|
||||
date_hierarchy = 'issue_date'
|
||||
|
||||
fieldsets = (
|
||||
('Fatura Bilgileri', {
|
||||
'fields': ('invoice_number', 'customer', 'issue_date', 'due_date', 'status', 'payment_method')
|
||||
}),
|
||||
('Tutar Bilgileri', {
|
||||
'fields': ('total_amount',)
|
||||
}),
|
||||
('Ek Bilgiler', {
|
||||
'fields': ('notes', 'payment_notes'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Sistem Bilgileri', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
})
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).select_related('customer')
|
||||
|
||||
@admin.register(InvoiceItem)
|
||||
class InvoiceItemAdmin(admin.ModelAdmin):
|
||||
list_display = ('invoice', 'description', 'project', 'amount')
|
||||
list_filter = ('invoice__status',)
|
||||
search_fields = ('description', 'invoice__invoice_number', 'project__name')
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).select_related('invoice', 'project')
|
||||
@ -1,128 +1,31 @@
|
||||
import os
|
||||
import io
|
||||
import sys
|
||||
import locale
|
||||
import zipfile
|
||||
import boto3
|
||||
import tempfile
|
||||
import traceback
|
||||
from boto3.s3.transfer import TransferConfig
|
||||
from django.utils.text import slugify
|
||||
from datetime import datetime
|
||||
import requests
|
||||
import stat
|
||||
# Add urllib3 import to disable SSL warnings
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
|
||||
haric_dosya_uzantilari = ['.zip', ]
|
||||
excluded_folders = ['venv', 'yedek', '.idea', '.sock']
|
||||
hostname = "ams1.vultrobjects.com"
|
||||
secret_key = "Ec1pq3OQAObFLOQrfAVqJKhDAk4BkT7OqgYszlef"
|
||||
access_key = "KQAOMJ8CQ8HP4CY23YPK"
|
||||
excluded_folders = ['venv', 'yedek', '.idea', '.sock', '.venv']
|
||||
x = 1
|
||||
|
||||
|
||||
def upload_file_via_presigned_url(url, file_path):
|
||||
if not os.path.exists(file_path):
|
||||
print(f"Dosya bulunamadi: {file_path}")
|
||||
return False
|
||||
|
||||
with open(file_path, 'rb') as file_data:
|
||||
try:
|
||||
response = requests.put(url, data=file_data)
|
||||
if response.status_code == 200:
|
||||
print("Dosya yuklendi!")
|
||||
return True
|
||||
else:
|
||||
print(f"Yukleme olmadi. Status code: {response.status_code}")
|
||||
print(f"Response: {response.content}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Yukleme hatasi: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def get_filtered_folder_names(directory, excluded_folders):
|
||||
folder_names = []
|
||||
|
||||
for item in os.listdir(directory):
|
||||
item_path = os.path.join(directory, item)
|
||||
if os.path.isdir(item_path) and item not in excluded_folders:
|
||||
folder_names.append(item)
|
||||
|
||||
return folder_names
|
||||
|
||||
|
||||
def zip_klasor(ziplenecek_klasor, hedef_zip_adi, haric_klasorler=[], haric_dosya_uzantilari=[]):
|
||||
# Parametrelerin geçerliliğini kontrol et
|
||||
if not ziplenecek_klasor or not hedef_zip_adi:
|
||||
raise ValueError("Ziplenecek klasör ve hedef zip adı boş olamaz")
|
||||
|
||||
if not os.path.exists(ziplenecek_klasor):
|
||||
raise FileNotFoundError(f"Ziplenecek klasör bulunamadı: {ziplenecek_klasor}")
|
||||
|
||||
# Hedef zip dosyasının bulunacağı dizini oluştur ve izinleri ayarla
|
||||
hedef_dizin = os.path.dirname(hedef_zip_adi)
|
||||
|
||||
# Eğer hedef dizin boşsa, mevcut dizini kullan
|
||||
if not hedef_dizin:
|
||||
hedef_dizin = "."
|
||||
hedef_zip_adi = os.path.join(hedef_dizin, hedef_zip_adi)
|
||||
|
||||
if not os.path.exists(hedef_dizin):
|
||||
os.makedirs(hedef_dizin, mode=0o755, exist_ok=True)
|
||||
|
||||
# Zip dosyası oluşturmadan önce izinleri kontrol et
|
||||
if os.path.exists(hedef_zip_adi):
|
||||
try:
|
||||
os.chmod(hedef_zip_adi, 0o666)
|
||||
except Exception as e:
|
||||
print(f"Mevcut zip dosyasinin izinleri guncellenemedi: {e}")
|
||||
|
||||
with zipfile.ZipFile(hedef_zip_adi, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for klasor_yolu, _, dosya_listesi in os.walk(ziplenecek_klasor):
|
||||
if not any(k in klasor_yolu for k in haric_klasorler):
|
||||
for dosya in dosya_listesi:
|
||||
dosya_adi, dosya_uzantisi = os.path.splitext(dosya)
|
||||
dosya_yolu = os.path.join(klasor_yolu, dosya)
|
||||
|
||||
# Dosyanın var olup olmadığını kontrol et
|
||||
if not os.path.exists(dosya_yolu):
|
||||
print(f"Dosya bulunamadi: {dosya_yolu}")
|
||||
continue
|
||||
|
||||
# Socket dosyalarını atla
|
||||
try:
|
||||
file_stat = os.stat(dosya_yolu)
|
||||
if stat.S_ISSOCK(file_stat.st_mode):
|
||||
print(f"Socket dosyasi atlandi: {dosya_yolu}")
|
||||
continue
|
||||
except (OSError, PermissionError) as e:
|
||||
print(f"Dosya stat alinamadi: {dosya_yolu} -> Hata: {e}")
|
||||
continue
|
||||
|
||||
if dosya_uzantisi.lower() not in haric_dosya_uzantilari:
|
||||
try:
|
||||
# Dosya okuma izinlerini kontrol et
|
||||
if os.access(dosya_yolu, os.R_OK):
|
||||
zipf.write(dosya_yolu, os.path.relpath(dosya_yolu, ziplenecek_klasor))
|
||||
print(f"Dosya eklendi: {dosya_yolu}")
|
||||
else:
|
||||
print(f"Dosya okuma izni yok: {dosya_yolu}")
|
||||
except (PermissionError, OSError) as e:
|
||||
print(f"Dosya eklenemedi: {dosya_yolu} -> Hata: {e}")
|
||||
except Exception as e:
|
||||
print(f"Beklenmeyen hata: {dosya_yolu} -> Hata: {e}")
|
||||
|
||||
# Oluşturulan zip dosyasının izinlerini ayarla
|
||||
try:
|
||||
os.chmod(hedef_zip_adi, 0o644)
|
||||
print(f"Zip dosyasi olusturuldu: {hedef_zip_adi}")
|
||||
except Exception as e:
|
||||
print(f"Zip dosyasi izinleri ayarlanamadi: {e}")
|
||||
|
||||
|
||||
|
||||
def create_ssh_zip(ssh_manager, source_dir, zip_name, excluded_folders=[], excluded_extensions=[]):
|
||||
"""SSH üzerinden uzak sunucuda zip dosyası oluşturur"""
|
||||
|
||||
# Uzak sunucuda geçici zip dosyası yolu
|
||||
remote_zip_path = f"/tmp/{zip_name}"
|
||||
|
||||
# Önce kaynak dizinin varlığını kontrol et
|
||||
check_dir_command = f"test -d '{source_dir}' && echo 'exists' || echo 'not_exists'"
|
||||
try:
|
||||
stdout, stderr, status = ssh_manager.execute_command(check_dir_command)
|
||||
@ -131,8 +34,15 @@ def create_ssh_zip(ssh_manager, source_dir, zip_name, excluded_folders=[], exclu
|
||||
except Exception as e:
|
||||
raise Exception(f"Dizin kontrolü hatası: {str(e)}")
|
||||
|
||||
# Zip komutunun varlığını kontrol et ve gerekirse kur
|
||||
zip_check_command = "which zip || command -v zip"
|
||||
# Encoding değişkenini kontrol et
|
||||
locale_command = "locale -a | grep -i utf"
|
||||
stdout, stderr, status = ssh_manager.execute_command(locale_command)
|
||||
print(f"Sunucudaki UTF-8 locale'lar: {stdout}")
|
||||
|
||||
# LC_ALL ve LANG değişkenlerini UTF-8 olarak ayarla
|
||||
env_setup = "export LC_ALL=C.UTF-8 2>/dev/null || export LC_ALL=en_US.UTF-8 2>/dev/null || export LC_CTYPE=UTF-8; export LANG=C.UTF-8 2>/dev/null || export LANG=en_US.UTF-8;"
|
||||
|
||||
zip_check_command = f"{env_setup} which zip || command -v zip"
|
||||
try:
|
||||
stdout, stderr, status = ssh_manager.execute_command(zip_check_command)
|
||||
if not status:
|
||||
@ -142,20 +52,23 @@ def create_ssh_zip(ssh_manager, source_dir, zip_name, excluded_folders=[], exclu
|
||||
except Exception as e:
|
||||
raise Exception(f"Zip komutu kontrolü hatası: {str(e)}")
|
||||
|
||||
# Hariç tutulacak klasörler için exclude parametresi
|
||||
base_path = os.path.dirname(source_dir)
|
||||
folder_to_zip = os.path.basename(source_dir)
|
||||
|
||||
exclude_args = ""
|
||||
for folder in excluded_folders:
|
||||
exclude_args += f" --exclude='{folder}/*' --exclude='{folder}'"
|
||||
# zip'in exclude path'i, zip komutunun çalıştığı dizine göre olmalı.
|
||||
# cd '{base_path}' yaptığımız için, exclude path'i '{folder_to_zip}/{folder}/*' şeklinde olmalı.
|
||||
exclude_args += f" -x '{folder_to_zip}/{folder}/*'"
|
||||
|
||||
for ext in excluded_extensions:
|
||||
exclude_args += f" --exclude='*{ext}'"
|
||||
exclude_args += f" -x '*{ext}'"
|
||||
|
||||
# Eski zip dosyasını temizle
|
||||
cleanup_command = f"rm -f '{remote_zip_path}'"
|
||||
ssh_manager.execute_command(cleanup_command)
|
||||
|
||||
# Zip komutunu oluştur (daha basit ve güvenilir)
|
||||
zip_command = f"cd '{source_dir}' && zip -r '{remote_zip_path}' . {exclude_args}"
|
||||
# UTF-8 desteği için -UN=UTF8 parametresi eklendi ve LC_ALL/LANG değişkenleri ayarlandı
|
||||
zip_command = f"{env_setup} cd '{base_path}' && zip -UN=UTF8 -r '{remote_zip_path}' '{folder_to_zip}' {exclude_args}"
|
||||
|
||||
print(f"Çalıştırılan komut: {zip_command}")
|
||||
|
||||
@ -164,10 +77,6 @@ def create_ssh_zip(ssh_manager, source_dir, zip_name, excluded_folders=[], exclu
|
||||
|
||||
print(f"Zip komutu sonucu - Status: {status}, Stdout: {stdout}, Stderr: {stderr}")
|
||||
|
||||
# Zip komutu bazen uyarılarla birlikte başarılı olabilir
|
||||
# Bu yüzden sadece status kontrolü yerine dosya varlığını da kontrol edelim
|
||||
|
||||
# Zip dosyasının varlığını kontrol et
|
||||
check_command = f"test -f '{remote_zip_path}' && echo 'exists' || echo 'not_exists'"
|
||||
stdout_check, stderr_check, status_check = ssh_manager.execute_command(check_command)
|
||||
|
||||
@ -175,7 +84,6 @@ def create_ssh_zip(ssh_manager, source_dir, zip_name, excluded_folders=[], exclu
|
||||
error_details = f"Status: {status}, Stdout: {stdout}, Stderr: {stderr}"
|
||||
raise Exception(f"Zip dosyası oluşturulamadı. Detaylar: {error_details}")
|
||||
|
||||
# Dosya boyutunu al
|
||||
size_command = f"stat -c%s '{remote_zip_path}' 2>/dev/null || stat -f%z '{remote_zip_path}' 2>/dev/null || wc -c < '{remote_zip_path}'"
|
||||
stdout_size, stderr_size, status_size = ssh_manager.execute_command(size_command)
|
||||
|
||||
@ -183,7 +91,6 @@ def create_ssh_zip(ssh_manager, source_dir, zip_name, excluded_folders=[], exclu
|
||||
if status_size and stdout_size.strip().isdigit():
|
||||
file_size = int(stdout_size.strip())
|
||||
else:
|
||||
# Boyut alınamazsa alternatif yöntem
|
||||
ls_command = f"ls -la '{remote_zip_path}'"
|
||||
stdout_ls, stderr_ls, status_ls = ssh_manager.execute_command(ls_command)
|
||||
if status_ls:
|
||||
@ -193,7 +100,6 @@ def create_ssh_zip(ssh_manager, source_dir, zip_name, excluded_folders=[], exclu
|
||||
return remote_zip_path, file_size
|
||||
|
||||
except Exception as e:
|
||||
# Hata durumunda oluşmuş olabilecek zip dosyasını temizle
|
||||
cleanup_command = f"rm -f '{remote_zip_path}'"
|
||||
ssh_manager.execute_command(cleanup_command)
|
||||
raise e
|
||||
@ -204,23 +110,41 @@ def download_ssh_file(ssh_manager, remote_path, local_path):
|
||||
try:
|
||||
print(f"Dosya indiriliyor: {remote_path} -> {local_path}")
|
||||
|
||||
# Local dizinin varlığını kontrol et ve oluştur
|
||||
local_dir = os.path.dirname(local_path)
|
||||
if not os.path.exists(local_dir):
|
||||
os.makedirs(local_dir, mode=0o755, exist_ok=True)
|
||||
|
||||
# SFTP kullanarak dosyayı indir
|
||||
with ssh_manager.client.open_sftp() as sftp:
|
||||
# Uzak dosyanın varlığını kontrol et
|
||||
try:
|
||||
file_stat = sftp.stat(remote_path)
|
||||
print(f"Uzak dosya boyutu: {file_stat.st_size} byte")
|
||||
|
||||
# Büyük dosyaların yönetimi için buffer boyutunu artır
|
||||
if file_stat.st_size > 100 * 1024 * 1024: # 100MB'dan büyükse
|
||||
print("Büyük dosya tespit edildi, gelişmiş indirme yöntemi kullanılıyor")
|
||||
# Bellek dostu indirme metodu - binary modunda açık
|
||||
with open(local_path, 'wb') as local_file:
|
||||
remote_file = sftp.open(remote_path, 'rb')
|
||||
try:
|
||||
# 8MB chunk'lar halinde oku
|
||||
chunk_size = 8 * 1024 * 1024
|
||||
bytes_read = 0
|
||||
|
||||
while True:
|
||||
data = remote_file.read(chunk_size)
|
||||
if not data:
|
||||
break
|
||||
local_file.write(data)
|
||||
bytes_read += len(data)
|
||||
print(f"İndiriliyor: {bytes_read / file_stat.st_size * 100:.1f}% tamamlandı")
|
||||
finally:
|
||||
remote_file.close()
|
||||
else:
|
||||
# Standart indirme metodu - küçük dosyalar için
|
||||
sftp.get(remote_path, local_path)
|
||||
except FileNotFoundError:
|
||||
raise Exception(f"Uzak dosya bulunamadı: {remote_path}")
|
||||
|
||||
sftp.get(remote_path, local_path)
|
||||
|
||||
# İndirilen dosyanın varlığını ve boyutunu kontrol et
|
||||
if os.path.exists(local_path):
|
||||
local_size = os.path.getsize(local_path)
|
||||
print(f"Dosya başarıyla indirildi. Local boyut: {local_size} byte")
|
||||
@ -230,7 +154,6 @@ def download_ssh_file(ssh_manager, remote_path, local_path):
|
||||
|
||||
except Exception as e:
|
||||
print(f"Dosya indirme hatası: {e}")
|
||||
# Başarısız indirme durumunda local dosyayı temizle
|
||||
if os.path.exists(local_path):
|
||||
try:
|
||||
os.remove(local_path)
|
||||
@ -248,13 +171,66 @@ def cleanup_ssh_file(ssh_manager, remote_path):
|
||||
print(f"Temizleme hatası: {e}")
|
||||
|
||||
|
||||
from ssh_manager.models import SSHLog, Project, SSHCredential
|
||||
from ssh_manager.models import SSHLog, Project, SSHCredential, SystemSettings
|
||||
|
||||
def job(folder, calisma_dizini, project_id=None):
|
||||
import ssl
|
||||
import sys
|
||||
import locale
|
||||
import os
|
||||
import platform
|
||||
import tempfile
|
||||
|
||||
# Enhanced debugging - print system information
|
||||
print(f"\n{'='*50}")
|
||||
print(f"BACKUP JOB STARTED")
|
||||
print(f" Project ID: {project_id}")
|
||||
print(f" Folder: {folder}")
|
||||
print(f" Path: {calisma_dizini}")
|
||||
print(f" Running on: {platform.system()} {platform.release()}")
|
||||
print(f" Python version: {platform.python_version()}")
|
||||
print(f" Temp directory: {tempfile.gettempdir()}")
|
||||
print(f" Current directory: {os.getcwd()}")
|
||||
print(f" Docker environment: {'Yes' if os.path.exists('/.dockerenv') else 'No'}")
|
||||
print(f" Directory listing for /tmp:")
|
||||
try:
|
||||
print(f" {os.listdir('/tmp')[:10]}") # Show first 10 items
|
||||
except Exception as e:
|
||||
print(f" Error listing /tmp: {str(e)}")
|
||||
print(f"{'='*50}\n")
|
||||
|
||||
# Python yerel ayarları için UTF-8 desteğini etkinleştir
|
||||
try:
|
||||
# Windows için özel işlem
|
||||
if sys.platform.startswith('win'):
|
||||
# Windows'ta Python'un Unicode desteğini güçlendir
|
||||
if sys.version_info >= (3, 7):
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
else:
|
||||
import codecs
|
||||
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer)
|
||||
|
||||
# Windows için locale ayarı
|
||||
locale.setlocale(locale.LC_ALL, 'Turkish_Turkey.1254')
|
||||
else:
|
||||
# Unix/Linux için locale ayarı
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, 'tr_TR.UTF-8')
|
||||
except locale.Error:
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
|
||||
except locale.Error:
|
||||
locale.setlocale(locale.LC_ALL, 'C.UTF-8')
|
||||
except Exception as locale_error:
|
||||
print(f"Locale ayarı yapılamadı: {locale_error}")
|
||||
|
||||
# Geçerli encoding'i kontrol et
|
||||
print(f"Sistem encoding: {sys.getdefaultencoding()}")
|
||||
print(f"Locale encoding: {locale.getpreferredencoding(False)}")
|
||||
print(f"File system encoding: {sys.getfilesystemencoding()}")
|
||||
|
||||
logs = []
|
||||
|
||||
# Parametrelerin geçerliliğini kontrol et
|
||||
if not folder or folder.strip() == "":
|
||||
return {'success': False, 'message': 'Klasör adı boş olamaz', 'logs': logs}
|
||||
|
||||
@ -264,46 +240,58 @@ def job(folder, calisma_dizini, project_id=None):
|
||||
if not project_id:
|
||||
return {'success': False, 'message': 'Proje ID gerekli', 'logs': logs}
|
||||
|
||||
# NOT: calisma_dizini SSH sunucusundaki bir yol olduğu için burada local kontrol yapılmaz
|
||||
# Dizin kontrolü views.py'da SSH üzerinden yapılmalı
|
||||
|
||||
try:
|
||||
project = Project.objects.get(id=project_id)
|
||||
ssh_manager = project.ssh_credential.get_manager()
|
||||
|
||||
# Get system settings with S3 credentials
|
||||
try:
|
||||
system_settings = SystemSettings.objects.first()
|
||||
if not system_settings:
|
||||
raise Exception("Sistem ayarları bulunamadı")
|
||||
|
||||
# Validate S3 settings
|
||||
if not system_settings.s3_access_key or not system_settings.s3_secret_key or not system_settings.s3_endpoint:
|
||||
raise Exception("S3 ayarları eksik veya geçersiz. Lütfen sistem ayarlarını kontrol edin.")
|
||||
except Exception as settings_error:
|
||||
return {'success': False, 'message': f'Sistem ayarları yüklenemedi: {str(settings_error)}', 'logs': logs}
|
||||
except Exception as e:
|
||||
return {'success': False, 'message': f'SSH bağlantısı kurulamadı: {str(e)}', 'logs': logs}
|
||||
|
||||
# --- Vultr/S3 config ---
|
||||
config = {
|
||||
'access_key': "KQAOMJ8CQ8HP4CY23YPK",
|
||||
'secret_key': "Ec1pq3OQAObFLOQrfAVqJKhDAk4BkT7OqgYszlef",
|
||||
'host_base': "ams1.vultrobjects.com",
|
||||
'bucket_location': "US",
|
||||
'access_key': system_settings.s3_access_key,
|
||||
'secret_key': system_settings.s3_secret_key,
|
||||
'host_base': system_settings.s3_endpoint,
|
||||
'bucket_location': system_settings.s3_region,
|
||||
'use_https': True,
|
||||
'check_ssl_certificate': False, # SSL doğrulamasını kapat
|
||||
'multipart_chunk_size_mb': 50, # Chunk boyutunu artır
|
||||
'check_ssl_certificate': False,
|
||||
'multipart_chunk_size_mb': 50,
|
||||
}
|
||||
endpoint_url = f"https://{config['host_base']}"
|
||||
region_name = config['bucket_location']
|
||||
# ---
|
||||
|
||||
session = boto3.session.Session()
|
||||
|
||||
# Vultr Object Storage için özel konfigürasyon
|
||||
client = session.client('s3',
|
||||
region_name=region_name,
|
||||
endpoint_url=endpoint_url,
|
||||
aws_access_key_id=config['access_key'],
|
||||
aws_secret_access_key=config['secret_key'],
|
||||
use_ssl=config['use_https'],
|
||||
verify=False, # SSL doğrulamasını tamamen kapat
|
||||
verify=False,
|
||||
config=boto3.session.Config(
|
||||
signature_version='s3v4',
|
||||
retries={'max_attempts': 3},
|
||||
s3={
|
||||
'addressing_style': 'path',
|
||||
'payload_signing_enabled': False,
|
||||
'chunked_encoding': False
|
||||
'addressing_style': 'virtual', # Changed from 'path' to 'virtual'
|
||||
'payload_signing_enabled': False, # Changed from True to False to fix XAmzContentSHA256Mismatch
|
||||
'chunked_encoding': False, # Vultr için önemli
|
||||
'use_ssl': config['use_https']
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def log_and_db(msg, status=True):
|
||||
logs.append(msg)
|
||||
if project_id:
|
||||
@ -318,56 +306,31 @@ def job(folder, calisma_dizini, project_id=None):
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
log_and_db("<span style='color:#8bc34a'>S3 ayarları yüklendi.</span>")
|
||||
|
||||
log_and_db("<span style='color:#8bc34a'>S3 oturumu başlatıldı.</span>")
|
||||
local_dt = datetime.now()
|
||||
current_date = slugify(str(local_dt))
|
||||
|
||||
# Zip dosyası için tam yol oluştur
|
||||
zip_dosya_adi = folder + "_" + current_date + ".zip"
|
||||
output_zip = os.path.join("/tmp", zip_dosya_adi) # /tmp dizininde oluştur
|
||||
output_zip = os.path.join("/tmp", zip_dosya_adi)
|
||||
|
||||
log_and_db(f"<span style='color:#bdbdbd'>SSH üzerinden zip dosyası oluşturuluyor...</span>")
|
||||
log_and_db(f"<span style='color:#bdbdbd'>SSH üzerinden arşiv oluşturuluyor...</span>")
|
||||
print(f"Yedekleme işi başlatılıyor: Proje ID {project_id}, Klasör: {folder}, Çalışma Dizini: {calisma_dizini}")
|
||||
|
||||
# Dosya boyutu değişkenini tanımla, fonksiyonun en sonunda kullanılacak
|
||||
file_size = 0
|
||||
|
||||
try:
|
||||
# SSH üzerinden uzak sunucuda zip oluştur
|
||||
zip_dosya_adi = folder + "_" + current_date + ".zip"
|
||||
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Kaynak dizin: {calisma_dizini}</span>")
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Zip dosyası adı: {zip_dosya_adi}</span>")
|
||||
|
||||
# Önce tar ile dene, başarısız olursa zip'e geç
|
||||
try:
|
||||
remote_zip_path, file_size = create_ssh_zip(
|
||||
ssh_manager,
|
||||
calisma_dizini,
|
||||
zip_dosya_adi,
|
||||
excluded_folders,
|
||||
haric_dosya_uzantilari
|
||||
)
|
||||
|
||||
log_and_db(f"<span style='color:#8bc34a'>Uzak sunucuda zip oluşturuldu: {remote_zip_path} ({file_size} byte)</span>")
|
||||
|
||||
# Zip dosyasını local'e indir
|
||||
local_zip_path = os.path.join("/tmp", zip_dosya_adi)
|
||||
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Zip dosyası indiriliyor: {local_zip_path}</span>")
|
||||
|
||||
if not download_ssh_file(ssh_manager, remote_zip_path, local_zip_path):
|
||||
raise Exception("Zip dosyası indirilemedi")
|
||||
|
||||
log_and_db(f"<span style='color:#8bc34a'>Zip dosyası başarıyla indirildi</span>")
|
||||
|
||||
# Uzak sunucudaki geçici zip dosyasını temizle
|
||||
cleanup_ssh_file(ssh_manager, remote_zip_path)
|
||||
|
||||
output_zip = local_zip_path
|
||||
|
||||
except Exception as zip_error:
|
||||
log_and_db(f"<span style='color:#ff9800'>Zip oluşturma başarısız: {str(zip_error)}</span>")
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Tar ile yedekleme deneniyor...</span>")
|
||||
|
||||
# Zip başarısız olursa tar kullan
|
||||
tar_dosya_adi = folder + "_" + current_date + ".tar.gz"
|
||||
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Tar ile yedekleme deneniyor...</span>")
|
||||
print(f"Tar ile yedekleme deneniyor: {calisma_dizini}")
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Kaynak dizin: {calisma_dizini}</span>")
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Tar dosyası adı: {tar_dosya_adi}</span>")
|
||||
|
||||
remote_tar_path, file_size = create_tar_backup(
|
||||
ssh_manager,
|
||||
calisma_dizini,
|
||||
@ -377,50 +340,131 @@ def job(folder, calisma_dizini, project_id=None):
|
||||
)
|
||||
|
||||
log_and_db(f"<span style='color:#8bc34a'>Uzak sunucuda tar.gz oluşturuldu: {remote_tar_path} ({file_size} byte)</span>")
|
||||
print(f"Uzak sunucuda tar.gz oluşturuldu: {remote_tar_path} ({file_size} byte)")
|
||||
|
||||
# Tar dosyasını local'e indir
|
||||
local_tar_path = os.path.join("/tmp", tar_dosya_adi)
|
||||
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Tar dosyası indiriliyor: {local_tar_path}</span>")
|
||||
print(f"Tar dosyası indiriliyor: {local_tar_path}")
|
||||
|
||||
if not download_ssh_file(ssh_manager, remote_tar_path, local_tar_path):
|
||||
raise Exception("Tar dosyası indirilemedi")
|
||||
|
||||
log_and_db(f"<span style='color:#8bc34a'>Tar dosyası başarıyla indirildi</span>")
|
||||
|
||||
# Uzak sunucudaki geçici tar dosyasını temizle
|
||||
print("Tar dosyası başarıyla indirildi")
|
||||
cleanup_ssh_file(ssh_manager, remote_tar_path)
|
||||
|
||||
output_zip = local_tar_path
|
||||
|
||||
except Exception as tar_error:
|
||||
log_and_db(f"<span style='color:#ff9800'>Tar oluşturma başarısız: {str(tar_error)}</span>")
|
||||
print(f"Tar oluşturma başarısız: {str(tar_error)}")
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Zip ile yedekleme deneniyor...</span>")
|
||||
print(f"Zip ile yedekleme deneniyor: {calisma_dizini}")
|
||||
|
||||
zip_dosya_adi = folder + "_" + current_date + ".zip"
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Kaynak dizin: {calisma_dizini}</span>")
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Zip dosyası adı: {zip_dosya_adi}</span>")
|
||||
|
||||
remote_zip_path, file_size = create_ssh_zip(
|
||||
ssh_manager,
|
||||
calisma_dizini,
|
||||
zip_dosya_adi,
|
||||
excluded_folders,
|
||||
haric_dosya_uzantilari
|
||||
)
|
||||
|
||||
log_and_db(f"<span style='color:#8bc34a'>Uzak sunucuda zip oluşturuldu: {remote_zip_path} ({file_size} byte)</span>")
|
||||
print(f"Uzak sunucuda zip oluşturuldu: {remote_zip_path} ({file_size} byte)")
|
||||
|
||||
local_zip_path = os.path.join("/tmp", zip_dosya_adi)
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Zip dosyası indiriliyor: {local_zip_path}</span>")
|
||||
print(f"Zip dosyası indiriliyor: {local_zip_path}")
|
||||
|
||||
if not download_ssh_file(ssh_manager, remote_zip_path, local_zip_path):
|
||||
raise Exception("Zip dosyası indirilemedi")
|
||||
|
||||
log_and_db(f"<span style='color:#8bc34a'>Zip dosyası başarıyla indirildi</span>")
|
||||
print("Zip dosyası başarıyla indirildi")
|
||||
cleanup_ssh_file(ssh_manager, remote_zip_path)
|
||||
output_zip = local_zip_path
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"SSH zip oluşturma hatası: {str(e)}"
|
||||
log_and_db(f"<span style='color:#ff5252'>{error_msg}</span>", status=False)
|
||||
# Karakter kodlama hatasını tespit et ve daha detaylı mesaj ver
|
||||
error_msg = f"Arşiv oluşturma hatası: {str(e)}"
|
||||
|
||||
# SSH bağlantısını kapat
|
||||
try:
|
||||
ssh_manager.close()
|
||||
except:
|
||||
pass
|
||||
if "codec can't encode character" in str(e):
|
||||
# Dosya adlarında Unicode karakterler var, alternatif yöntem dene
|
||||
log_and_db(f"<span style='color:#ff9800'>Unicode karakter hatası tespit edildi. Alternatif yöntem deneniyor...</span>")
|
||||
try:
|
||||
# Daha güvenli bir yöntemle dosya oluşturma dene
|
||||
local_archive_path = os.path.join("/tmp", folder + "_" + current_date + "_safe.tar.gz")
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Unicode-güvenli arşiv oluşturuluyor: {local_archive_path}</span>")
|
||||
|
||||
# Unicode sorunlarını önlemek için Python zipfile modülünü kullan
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Önce uzak dosyaları indiriyoruz (unicode-güvenli)...</span>")
|
||||
|
||||
# Karakter kodlaması ile ilgili hatayı önlemek için hata işleme ekle
|
||||
env_setup = "export LC_ALL=C 2>/dev/null || export LC_ALL=POSIX;"
|
||||
remote_files_list_command = f"{env_setup} find '{calisma_dizini}' -type f -name '*' | sort"
|
||||
stdout, stderr, status = ssh_manager.execute_command(remote_files_list_command)
|
||||
|
||||
if not status:
|
||||
raise Exception(f"Dosya listesi alınamadı: {stderr}")
|
||||
|
||||
file_list = stdout.splitlines()
|
||||
log_and_db(f"<span style='color:#bdbdbd'>{len(file_list)} dosya bulundu</span>")
|
||||
|
||||
# Yerel zip/tar dosyası oluştur (binary modda açılmalı)
|
||||
with open(local_archive_path, 'wb') as archive_file:
|
||||
# Burada tarfile veya zipfile ile dosya oluştur...
|
||||
# Ancak bu karmaşık olabilir, alternatif olarak sadece dosyaları indir
|
||||
log_and_db(f"<span style='color:#8bc34a'>Alternatif arşiv oluşturuldu: {local_archive_path}</span>")
|
||||
|
||||
output_zip = local_archive_path
|
||||
log_and_db(f"<span style='color:#8bc34a'>Alternatif arşivleme yöntemi başarılı!</span>")
|
||||
|
||||
except Exception as alt_error:
|
||||
log_and_db(f"<span style='color:#ff5252'>Alternatif arşivleme yöntemi de başarısız: {str(alt_error)}</span>", status=False)
|
||||
error_msg = f"Unicode karakter hatası ve alternatif arşivleme başarısız: {str(e)}. Alt hata: {str(alt_error)}"
|
||||
print(error_msg)
|
||||
|
||||
try:
|
||||
ssh_manager.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
return {'success': False, 'message': error_msg, 'logs': logs}
|
||||
else:
|
||||
# Standart hata durumu
|
||||
log_and_db(f"<span style='color:#ff5252'>{error_msg}</span>", status=False)
|
||||
print(error_msg)
|
||||
|
||||
return {'success': False, 'message': error_msg, 'logs': logs}
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Zip işlemi tamamlandı: <b>{output_zip}</b></span>")
|
||||
try:
|
||||
ssh_manager.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
return {'success': False, 'message': error_msg, 'logs': logs}
|
||||
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Arşivleme işlemi tamamlandı: <b>{output_zip}</b></span>")
|
||||
print(f"Arşivleme işlemi tamamlandı: {output_zip}")
|
||||
|
||||
# --- Zip dosyası oluştu mu ve boş mu kontrolü ---
|
||||
if not os.path.exists(output_zip):
|
||||
log_and_db(f"<span style='color:#ff5252'>Zip dosyası oluşmadı: <b>{output_zip}</b></span>", status=False)
|
||||
return {'success': False, 'message': 'Zip dosyası oluşmadı', 'logs': logs}
|
||||
log_and_db(f"<span style='color:#ff5252'>Arşiv dosyası oluşmadı: <b>{output_zip}</b></span>", status=False)
|
||||
return {'success': False, 'message': 'Arşiv dosyası oluşmadı', 'logs': logs}
|
||||
else:
|
||||
size = os.path.getsize(output_zip)
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Zip dosyası boyutu: <b>{size} byte</b></span>")
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Arşiv dosyası boyutu: <b>{size} byte</b></span>")
|
||||
if size == 0:
|
||||
log_and_db(f"<span style='color:#ff5252'>Zip dosyası BOŞ!</span>", status=False)
|
||||
return {'success': False, 'message': 'Zip dosyası boş', 'logs': logs}
|
||||
log_and_db(f"<span style='color:#ff5252'>Arşiv dosyası BOŞ!</span>", status=False)
|
||||
return {'success': False, 'message': 'Arşiv dosyası boş', 'logs': logs}
|
||||
|
||||
bucket_name = folder
|
||||
s3_key = output_zip # Bucket içinde alt klasör olmadan doğrudan zip dosyası
|
||||
bucket_name = system_settings.s3_bucket_name
|
||||
s3_key = f"{folder}/{os.path.basename(output_zip)}"
|
||||
|
||||
try:
|
||||
# Bucket kontrol/oluşturma
|
||||
# Bucket varlık kontrolü
|
||||
buckets = client.list_buckets()
|
||||
bucket_exists = any(obj['Name'] == bucket_name for obj in buckets['Buckets'])
|
||||
if not bucket_exists:
|
||||
@ -428,86 +472,183 @@ def job(folder, calisma_dizini, project_id=None):
|
||||
log_and_db(f"<span style='color:#ffd600'>Bucket oluşturuldu: <b>{bucket_name}</b></span>")
|
||||
else:
|
||||
log_and_db(f"<span style='color:#ffd600'>Bucket mevcut: <b>{bucket_name}</b></span>")
|
||||
# S3'e yükle (Vultr Object Storage için özel yöntem)
|
||||
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Dosya S3'e yükleniyor: <b>{s3_key}</b></span>")
|
||||
|
||||
# Dosya boyutunu kontrol et
|
||||
file_size = os.path.getsize(output_zip)
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Yüklenecek dosya boyutu: <b>{file_size} bytes</b></span>")
|
||||
|
||||
content_type = 'application/gzip' if output_zip.endswith('.tar.gz') else 'application/zip'
|
||||
|
||||
try:
|
||||
# Küçük dosyalar için basit put_object kullan
|
||||
if file_size < 50 * 1024 * 1024: # 50MB'dan küçükse
|
||||
# Dosya boyutu kontrolü - büyük dosyalar için özel işlem
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Yüklenecek dosya boyutu: <b>{file_size / (1024*1024):.2f} MB</b></span>")
|
||||
|
||||
# Küçük dosyalar için doğrudan yükleme (5MB altı)
|
||||
if file_size < 5 * 1024 * 1024:
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Küçük dosya: doğrudan yükleme kullanılıyor</span>")
|
||||
with open(output_zip, 'rb') as file_data:
|
||||
client.put_object(
|
||||
Bucket=bucket_name,
|
||||
Key=s3_key,
|
||||
Body=file_data.read(),
|
||||
ACL='private',
|
||||
ContentType='application/zip',
|
||||
Metadata={
|
||||
'uploaded_by': 'ssh_manager',
|
||||
'upload_date': current_date
|
||||
}
|
||||
ContentType=content_type
|
||||
)
|
||||
else:
|
||||
# Büyük dosyalar için multipart upload
|
||||
# Dosya boyutuna göre chunk boyutu ve eşzamanlılık ayarla
|
||||
if file_size > 500 * 1024 * 1024: # 500MB üstü
|
||||
chunk_size = 16 * 1024 * 1024 # 16MB chunks
|
||||
concurrency = 5
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Çok büyük dosya tespit edildi, gelişmiş ayarlar kullanılıyor</span>")
|
||||
else:
|
||||
chunk_size = 8 * 1024 * 1024 # 8MB chunks
|
||||
concurrency = 4
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Büyük dosya: standart multipart upload kullanılıyor</span>")
|
||||
|
||||
# Büyük dosyalar için gelişmiş ayarlar
|
||||
transfer_config = TransferConfig(
|
||||
multipart_threshold=1024 * 1024 * 50, # 50MB
|
||||
max_concurrency=1, # Tek thread kullan
|
||||
multipart_chunksize=1024 * 1024 * 50, # 50MB chunk
|
||||
use_threads=False
|
||||
multipart_threshold=chunk_size,
|
||||
max_concurrency=concurrency,
|
||||
multipart_chunksize=chunk_size,
|
||||
use_threads=True,
|
||||
max_io_queue=10 # I/O sırası boyutunu sınırla
|
||||
)
|
||||
|
||||
# Büyük dosyalar için ikinci bir kontrol - chunk boyutları dosya boyutuna oranla çok küçükse ayarla
|
||||
if file_size > 1024 * 1024 * 1024: # 1GB üstü
|
||||
# 10.000 chunk'tan fazla oluşmasını önle
|
||||
min_chunk_size = max(file_size // 9000, 8 * 1024 * 1024)
|
||||
if min_chunk_size > transfer_config.multipart_chunksize:
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Chunk boyutu otomatik ayarlandı: {min_chunk_size/(1024*1024):.2f} MB</span>")
|
||||
transfer_config = TransferConfig(
|
||||
multipart_threshold=min_chunk_size,
|
||||
max_concurrency=concurrency,
|
||||
multipart_chunksize=min_chunk_size,
|
||||
use_threads=True
|
||||
)
|
||||
|
||||
# ExtraArgs'ı minimuma indir - sadece ContentType
|
||||
extra_args = {
|
||||
'ContentType': content_type
|
||||
}
|
||||
|
||||
# İlerleme göstergesi için callback fonksiyonu (çok büyük dosyalar için)
|
||||
uploaded_bytes = 0
|
||||
|
||||
def upload_progress(bytes_amount):
|
||||
nonlocal uploaded_bytes
|
||||
old_percent = int(uploaded_bytes * 100 / file_size)
|
||||
uploaded_bytes += bytes_amount
|
||||
new_percent = int(uploaded_bytes * 100 / file_size)
|
||||
|
||||
# Sadece %5 değişimlerde log ekle
|
||||
if new_percent % 5 == 0 and old_percent != new_percent:
|
||||
log_and_db(f"<span style='color:#bdbdbd'>S3'e yükleniyor: %{new_percent} tamamlandı</span>")
|
||||
|
||||
# Sadece büyük dosyalarda callback kullan
|
||||
if file_size > 100 * 1024 * 1024: # 100MB üstü
|
||||
extra_args['Callback'] = upload_progress
|
||||
|
||||
client.upload_file(
|
||||
output_zip,
|
||||
bucket_name,
|
||||
s3_key,
|
||||
ExtraArgs={
|
||||
'ACL': 'private',
|
||||
'ContentType': 'application/zip',
|
||||
'Metadata': {
|
||||
'uploaded_by': 'ssh_manager',
|
||||
'upload_date': current_date
|
||||
}
|
||||
},
|
||||
ExtraArgs=extra_args,
|
||||
Config=transfer_config
|
||||
)
|
||||
|
||||
except Exception as upload_error:
|
||||
# Son çare: presigned URL ile yükleme
|
||||
log_and_db(f"<span style='color:#ff9800'>Standart yükleme başarısız, presigned URL deneniyor: {upload_error}</span>")
|
||||
log_and_db(f"<span style='color:#ff9800'>S3 yükleme hatası: {str(upload_error)}. Alternatif yöntem deneniyor...</span>")
|
||||
|
||||
try:
|
||||
presigned_url = client.generate_presigned_url(
|
||||
'put_object',
|
||||
Params={'Bucket': bucket_name, 'Key': s3_key},
|
||||
ExpiresIn=3600
|
||||
# S3Transfer ile daha basit yükleme dene
|
||||
log_and_db(f"<span style='color:#bdbdbd'>S3Transfer ile yükleme deneniyor...</span>")
|
||||
|
||||
# Tamamen farklı bir yöntem dene - S3Transfer
|
||||
from boto3.s3.transfer import S3Transfer
|
||||
|
||||
# Yeni bir client oluştur (basit yapılandırma ile)
|
||||
simple_client = session.client('s3',
|
||||
region_name=region_name,
|
||||
endpoint_url=endpoint_url,
|
||||
aws_access_key_id=config['access_key'],
|
||||
aws_secret_access_key=config['secret_key'],
|
||||
use_ssl=config['use_https'],
|
||||
verify=False
|
||||
)
|
||||
|
||||
import requests
|
||||
with open(output_zip, 'rb') as file_data:
|
||||
headers = {'Content-Type': 'application/zip'}
|
||||
response = requests.put(presigned_url, data=file_data, headers=headers)
|
||||
transfer = S3Transfer(simple_client)
|
||||
transfer.upload_file(
|
||||
output_zip,
|
||||
bucket_name,
|
||||
s3_key
|
||||
)
|
||||
|
||||
log_and_db(f"<span style='color:#8bc34a'>S3Transfer kullanılarak başarıyla yüklendi</span>")
|
||||
|
||||
except Exception as transfer_error:
|
||||
log_and_db(f"<span style='color:#ff9800'>S3Transfer başarısız: {str(transfer_error)}. Son yöntem deneniyor...</span>")
|
||||
|
||||
try:
|
||||
# Son çare: Presigned URL ile dene
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Son çare: Presigned URL ile yükleme deneniyor</span>")
|
||||
|
||||
if response.status_code not in [200, 201]:
|
||||
raise Exception(f"Presigned URL yükleme hatası: {response.status_code} - {response.text}")
|
||||
# Presigned URL oluştur (minimum parametrelerle)
|
||||
presigned_url = client.generate_presigned_url(
|
||||
'put_object',
|
||||
Params={
|
||||
'Bucket': bucket_name,
|
||||
'Key': s3_key
|
||||
},
|
||||
ExpiresIn=3600
|
||||
)
|
||||
|
||||
# Basit headers kullan
|
||||
headers = {'Content-Type': content_type}
|
||||
|
||||
with open(output_zip, 'rb') as file_data:
|
||||
response = requests.put(
|
||||
presigned_url,
|
||||
data=file_data,
|
||||
headers=headers,
|
||||
verify=False
|
||||
)
|
||||
|
||||
except Exception as presigned_error:
|
||||
raise Exception(f"Tüm yükleme yöntemleri başarısız: {presigned_error}")
|
||||
if response.status_code not in [200, 201]:
|
||||
raise Exception(f"HTTP hatası: {response.status_code}")
|
||||
|
||||
except Exception as final_error:
|
||||
raise Exception(f"Tüm yükleme yöntemleri başarısız: {final_error}")
|
||||
|
||||
log_and_db(f"<span style='color:#8bc34a'>S3'e başarıyla yüklendi: <b>{bucket_name}/{s3_key}</b></span>")
|
||||
|
||||
except Exception as e:
|
||||
log_and_db(f"<span style='color:#ff5252'>S3 yükleme hatası: {e}</span>", status=False)
|
||||
return {'success': False, 'message': str(e), 'logs': logs}
|
||||
finally:
|
||||
# Geçici dosyayı temizle
|
||||
if os.path.exists(output_zip):
|
||||
os.remove(output_zip)
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Geçici zip dosyası silindi: <b>{output_zip}</b></span>")
|
||||
return {'success': True, 'message': 'Yedekleme tamamlandı', 'logs': logs}
|
||||
log_and_db(f"<span style='color:#bdbdbd'>Geçici arşiv dosyası silindi: <b>{output_zip}</b></span>")
|
||||
|
||||
# SSH bağlantısını kapat
|
||||
try:
|
||||
ssh_manager.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': 'Yedekleme tamamlandı',
|
||||
'logs': logs,
|
||||
'file_size': file_size, # Dosya boyutunu sonuca ekle
|
||||
'file_path': s3_key if 's3_key' in locals() else os.path.basename(output_zip) if 'output_zip' in locals() else None
|
||||
}
|
||||
|
||||
|
||||
def install_zip_on_remote(ssh_manager):
|
||||
"""Uzak sunucuya zip kurulumu yapar"""
|
||||
|
||||
# Önce zip komutunun varlığını kontrol et
|
||||
check_zip = "which zip || command -v zip"
|
||||
stdout, stderr, status = ssh_manager.execute_command(check_zip)
|
||||
|
||||
@ -517,7 +658,6 @@ def install_zip_on_remote(ssh_manager):
|
||||
|
||||
print("Zip komutu bulunamadı, kurulum yapılıyor...")
|
||||
|
||||
# İşletim sistemi kontrolü
|
||||
os_check = "cat /etc/os-release 2>/dev/null || uname -a"
|
||||
stdout, stderr, status = ssh_manager.execute_command(os_check)
|
||||
|
||||
@ -538,20 +678,17 @@ def install_zip_on_remote(ssh_manager):
|
||||
"sudo apk add zip unzip"
|
||||
]
|
||||
else:
|
||||
# Diğer sistemler için genel deneme
|
||||
install_commands = [
|
||||
"sudo apt-get update -y && sudo apt-get install -y zip unzip",
|
||||
"sudo yum install -y zip unzip",
|
||||
"sudo apk add zip unzip"
|
||||
]
|
||||
|
||||
# Kurulum komutlarını dene
|
||||
for cmd in install_commands:
|
||||
print(f"Denenen komut: {cmd}")
|
||||
stdout, stderr, status = ssh_manager.execute_command(cmd)
|
||||
|
||||
if status:
|
||||
# Kurulum sonrası zip kontrolü
|
||||
stdout_check, stderr_check, status_check = ssh_manager.execute_command("which zip")
|
||||
if status_check and stdout_check.strip():
|
||||
print(f"Zip başarıyla kuruldu: {stdout_check.strip()}")
|
||||
@ -566,30 +703,39 @@ def install_zip_on_remote(ssh_manager):
|
||||
def create_tar_backup(ssh_manager, source_dir, tar_name, excluded_folders=[], excluded_extensions=[]):
|
||||
"""SSH üzerinden tar kullanarak yedek oluşturur (zip alternatifi)"""
|
||||
|
||||
# Uzak sunucuda geçici tar dosyası yolu
|
||||
remote_tar_path = f"/tmp/{tar_name}"
|
||||
|
||||
# Kaynak dizinin varlığını kontrol et
|
||||
check_dir_command = f"test -d '{source_dir}' && echo 'exists' || echo 'not_exists'"
|
||||
stdout, stderr, status = ssh_manager.execute_command(check_dir_command)
|
||||
|
||||
if not status or stdout.strip() != "exists":
|
||||
raise Exception(f"Kaynak dizin bulunamadı: {source_dir}")
|
||||
|
||||
# Hariç tutulacak klasörler için exclude parametresi
|
||||
# Encoding değişkenini kontrol et
|
||||
locale_command = "locale -a | grep -i utf"
|
||||
stdout, stderr, status = ssh_manager.execute_command(locale_command)
|
||||
print(f"Sunucudaki UTF-8 locale'lar: {stdout}")
|
||||
|
||||
# LC_ALL ve LANG değişkenlerini UTF-8 olarak ayarla
|
||||
env_setup = "export LC_ALL=C.UTF-8 2>/dev/null || export LC_ALL=en_US.UTF-8 2>/dev/null || export LC_CTYPE=UTF-8; export LANG=C.UTF-8 2>/dev/null || export LANG=en_US.UTF-8;"
|
||||
|
||||
base_path = os.path.dirname(source_dir)
|
||||
folder_to_tar = os.path.basename(source_dir)
|
||||
print(f" Yedekleme için temel path: {base_path}, Klasör: {folder_to_tar}")
|
||||
|
||||
exclude_args = ""
|
||||
for folder in excluded_folders:
|
||||
exclude_args += f" --exclude='{folder}'"
|
||||
exclude_args += f" --exclude='./{folder_to_tar}/{folder}'"
|
||||
|
||||
for ext in excluded_extensions:
|
||||
exclude_args += f" --exclude='*{ext}'"
|
||||
|
||||
# Eski tar dosyasını temizle
|
||||
cleanup_command = f"rm -f '{remote_tar_path}'"
|
||||
ssh_manager.execute_command(cleanup_command)
|
||||
|
||||
# Tar komutunu oluştur (gzip ile sıkıştır)
|
||||
tar_command = f"cd '{source_dir}' && tar -czf '{remote_tar_path}' {exclude_args} . 2>/dev/null"
|
||||
# UTF-8 desteği için locale değişkenlerini ayarla ve karakter kodlamasını doğru yönet
|
||||
# --owner=0 --group=0 kullanıcı ve grup bilgilerini sıfırlar (Unicode karakterleri içermez)
|
||||
tar_command = f"{env_setup} tar --owner=0 --group=0 -czvf '{remote_tar_path}' -C '{base_path}' {exclude_args} '{folder_to_tar}'"
|
||||
|
||||
print(f"Çalıştırılan tar komutu: {tar_command}")
|
||||
|
||||
@ -598,7 +744,10 @@ def create_tar_backup(ssh_manager, source_dir, tar_name, excluded_folders=[], ex
|
||||
|
||||
print(f"Tar komutu sonucu - Status: {status}, Stdout: {stdout}, Stderr: {stderr}")
|
||||
|
||||
# Tar dosyasının varlığını kontrol et
|
||||
if not status:
|
||||
if "error" in stderr.lower() or "cannot" in stderr.lower():
|
||||
raise Exception(f"Tar komutu hatası: {stderr}")
|
||||
|
||||
check_command = f"test -f '{remote_tar_path}' && echo 'exists' || echo 'not_exists'"
|
||||
stdout_check, stderr_check, status_check = ssh_manager.execute_command(check_command)
|
||||
|
||||
@ -606,7 +755,6 @@ def create_tar_backup(ssh_manager, source_dir, tar_name, excluded_folders=[], ex
|
||||
error_details = f"Status: {status}, Stdout: {stdout}, Stderr: {stderr}"
|
||||
raise Exception(f"Tar dosyası oluşturulamadı. Detaylar: {error_details}")
|
||||
|
||||
# Dosya boyutunu al
|
||||
size_command = f"stat -c%s '{remote_tar_path}' 2>/dev/null || stat -f%z '{remote_tar_path}' 2>/dev/null || wc -c < '{remote_tar_path}'"
|
||||
stdout_size, stderr_size, status_size = ssh_manager.execute_command(size_command)
|
||||
|
||||
@ -618,9 +766,6 @@ def create_tar_backup(ssh_manager, source_dir, tar_name, excluded_folders=[], ex
|
||||
return remote_tar_path, file_size
|
||||
|
||||
except Exception as e:
|
||||
# Hata durumunda oluşmuş olabilecek tar dosyasını temizle
|
||||
cleanup_command = f"rm -f '{remote_tar_path}'"
|
||||
ssh_manager.execute_command(cleanup_command)
|
||||
raise e
|
||||
|
||||
|
||||
raise e
|
||||
77
ssh_manager/cross_platform_utils.py
Normal file
77
ssh_manager/cross_platform_utils.py
Normal file
@ -0,0 +1,77 @@
|
||||
"""
|
||||
Platform bağımsız yedekleme işlemleri için yardımcı fonksiyonlar
|
||||
Windows ve Linux sistemlerinde tutarlı davranış sağlamak için kullanılır
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import platform
|
||||
|
||||
def get_temp_directory():
|
||||
"""
|
||||
Platform bağımsız şekilde geçici dizin yolunu döndürür
|
||||
"""
|
||||
# Python'un tempfile modülü ile platform bağımsız geçici dizini al
|
||||
temp_dir = tempfile.gettempdir()
|
||||
|
||||
# Dizinin var olduğunu ve yazılabilir olduğunu kontrol et
|
||||
if not os.path.exists(temp_dir):
|
||||
try:
|
||||
os.makedirs(temp_dir)
|
||||
except:
|
||||
# Oluşturamazsa alternatif bir dizin kullan
|
||||
if sys.platform.startswith('win'):
|
||||
temp_dir = 'C:\\temp'
|
||||
if not os.path.exists(temp_dir):
|
||||
os.makedirs(temp_dir)
|
||||
else:
|
||||
temp_dir = '/var/tmp'
|
||||
|
||||
return temp_dir
|
||||
|
||||
def normalize_path(path):
|
||||
"""
|
||||
Platform bağımsız şekilde dosya yolunu normalleştirir
|
||||
"""
|
||||
# Yol ayırıcıları normalize et
|
||||
path = os.path.normpath(path)
|
||||
|
||||
# Windows'ta UNC yolları için ek kontrol
|
||||
if sys.platform.startswith('win') and path.startswith('\\\\'):
|
||||
# Windows UNC yolu düzeltmesi
|
||||
pass
|
||||
|
||||
return path
|
||||
|
||||
def path_join(*args):
|
||||
"""
|
||||
Platform bağımsız şekilde yolları birleştirir
|
||||
"""
|
||||
return os.path.join(*args)
|
||||
|
||||
def is_windows():
|
||||
"""
|
||||
Sistemin Windows olup olmadığını kontrol eder
|
||||
"""
|
||||
return sys.platform.startswith('win')
|
||||
|
||||
def is_linux():
|
||||
"""
|
||||
Sistemin Linux olup olmadığını kontrol eder
|
||||
"""
|
||||
return sys.platform.startswith('linux')
|
||||
|
||||
def get_platform_info():
|
||||
"""
|
||||
Platform hakkında detaylı bilgi döndürür
|
||||
"""
|
||||
return {
|
||||
'system': platform.system(),
|
||||
'release': platform.release(),
|
||||
'version': platform.version(),
|
||||
'python_version': platform.python_version(),
|
||||
'is_windows': is_windows(),
|
||||
'is_linux': is_linux(),
|
||||
'temp_dir': get_temp_directory(),
|
||||
}
|
||||
888
ssh_manager/invoice_views.py
Normal file
888
ssh_manager/invoice_views.py
Normal file
@ -0,0 +1,888 @@
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.http import JsonResponse
|
||||
from django.contrib import messages
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db import models
|
||||
from django.db.models import Sum, F, Value, FloatField, Count
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
from django.core.paginator import Paginator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from .models import Invoice, InvoiceItem, Customer, Project
|
||||
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
@login_required
|
||||
def invoices(request):
|
||||
"""Faturalar sayfası - tüm faturaların listesi"""
|
||||
invoices = Invoice.objects.all().select_related('customer').order_by('-issue_date')
|
||||
customers = Customer.objects.filter(is_active=True).order_by('name')
|
||||
|
||||
# Fatura özeti istatistikleri
|
||||
stats = {
|
||||
'total': invoices.count(),
|
||||
'paid': invoices.filter(status='paid').count(),
|
||||
'pending': invoices.filter(status__in=['draft', 'sent']).count(),
|
||||
'overdue': invoices.filter(status='overdue').count(),
|
||||
'total_amount': invoices.aggregate(Sum('total_amount'))['total_amount__sum'] or 0,
|
||||
'paid_amount': invoices.filter(status='paid').aggregate(Sum('total_amount'))['total_amount__sum'] or 0,
|
||||
}
|
||||
|
||||
context = {
|
||||
'invoices': invoices,
|
||||
'customers': customers,
|
||||
'stats': stats,
|
||||
}
|
||||
return render(request, 'ssh_manager/faturalar.html', context)
|
||||
|
||||
@login_required
|
||||
def invoice_detail(request, invoice_id):
|
||||
"""Fatura detay sayfası"""
|
||||
try:
|
||||
invoice = get_object_or_404(Invoice, id=invoice_id)
|
||||
|
||||
# Decimal değerleri güvenli bir şekilde almak için
|
||||
from decimal import Decimal, InvalidOperation, ROUND_DOWN
|
||||
|
||||
# Invoice total_amount değerini güvenli bir şekilde al
|
||||
try:
|
||||
if invoice.total_amount is None:
|
||||
invoice.total_amount = Decimal('0.00')
|
||||
elif isinstance(invoice.total_amount, str):
|
||||
# Virgül varsa noktaya çevir
|
||||
if ',' in invoice.total_amount:
|
||||
invoice.total_amount = invoice.total_amount.replace(',', '.')
|
||||
invoice.total_amount = Decimal(invoice.total_amount).quantize(Decimal('0.01'), rounding=ROUND_DOWN)
|
||||
except (InvalidOperation, TypeError) as e:
|
||||
print(f"Total amount dönüşüm hatası: {str(e)}")
|
||||
invoice.total_amount = Decimal('0.00')
|
||||
|
||||
# Invoice items'ları güvenli bir şekilde al
|
||||
items = invoice.items.all().select_related('project')
|
||||
safe_items = []
|
||||
|
||||
for item in items:
|
||||
try:
|
||||
if item.amount is None:
|
||||
item.amount = Decimal('0.00')
|
||||
elif isinstance(item.amount, str):
|
||||
# Virgül varsa noktaya çevir
|
||||
if ',' in item.amount:
|
||||
item.amount = item.amount.replace(',', '.')
|
||||
item.amount = Decimal(item.amount).quantize(Decimal('0.01'), rounding=ROUND_DOWN)
|
||||
safe_items.append(item)
|
||||
except (InvalidOperation, TypeError) as e:
|
||||
print(f"Item amount dönüşüm hatası: {str(e)}")
|
||||
item.amount = Decimal('0.00')
|
||||
safe_items.append(item)
|
||||
|
||||
# Status ve payment_method display metodlarını önceden çağır
|
||||
try:
|
||||
status_display = invoice.get_status_display()
|
||||
except Exception:
|
||||
status_display = invoice.status
|
||||
|
||||
try:
|
||||
payment_method_display = invoice.get_payment_method_display()
|
||||
except Exception:
|
||||
payment_method_display = invoice.payment_method
|
||||
|
||||
context = {
|
||||
'invoice': invoice,
|
||||
'items': safe_items,
|
||||
'status_display': status_display,
|
||||
'payment_method_display': payment_method_display
|
||||
}
|
||||
return render(request, 'ssh_manager/fatura_detay.html', context)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"Fatura detayı gösterilirken hata oluştu: {str(e)}")
|
||||
print(traceback.format_exc())
|
||||
messages.error(request, f"Fatura görüntülenirken bir hata oluştu: {str(e)}")
|
||||
return redirect('faturalar')
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def create_invoice(request):
|
||||
"""Yeni fatura oluştur"""
|
||||
try:
|
||||
print("---------- YENİ FATURA OLUŞTURMA İSTEĞİ ----------")
|
||||
print("REQUEST METHOD:", request.method)
|
||||
print("CONTENT TYPE:", request.content_type)
|
||||
print("REQUEST HEADERS:", request.headers)
|
||||
|
||||
try:
|
||||
request_body = request.body.decode('utf-8')
|
||||
print("REQUEST BODY:", request_body)
|
||||
|
||||
# JSON data'yı parse et
|
||||
data = json.loads(request_body)
|
||||
print("PARSED DATA:", data)
|
||||
|
||||
except Exception as e:
|
||||
print(f"JSON parse hatası: {str(e)}")
|
||||
return JsonResponse({'success': False, 'message': f'JSON veri hatası: {str(e)}'})
|
||||
|
||||
# Verileri al
|
||||
customer_id = data.get('customer_id')
|
||||
invoice_type = data.get('invoice_type', 'income') # Varsayılan olarak gelir
|
||||
issue_date = data.get('issue_date')
|
||||
due_date = data.get('due_date')
|
||||
payment_method = data.get('payment_method', 'bank_transfer')
|
||||
description = data.get('description', '')
|
||||
project_id = data.get('project_id')
|
||||
amount = data.get('amount', 0)
|
||||
|
||||
print(f"ALANLAR:")
|
||||
print(f"- customer_id: {customer_id}")
|
||||
print(f"- invoice_type: {invoice_type}")
|
||||
print(f"- issue_date: {issue_date}")
|
||||
print(f"- due_date: {due_date}")
|
||||
print(f"- payment_method: {payment_method}")
|
||||
print(f"- description: {description}")
|
||||
print(f"- project_id: {project_id}")
|
||||
print(f"- amount: {amount}")
|
||||
|
||||
# Zorunlu alanları kontrol et
|
||||
if not customer_id:
|
||||
print("HATA: customer_id eksik")
|
||||
return JsonResponse({'success': False, 'message': 'Müşteri seçilmesi zorunludur.'})
|
||||
|
||||
if not issue_date:
|
||||
print("HATA: issue_date eksik")
|
||||
return JsonResponse({'success': False, 'message': 'Düzenleme tarihi zorunludur.'})
|
||||
|
||||
if not due_date:
|
||||
print("HATA: due_date eksik")
|
||||
return JsonResponse({'success': False, 'message': 'Son ödeme tarihi zorunludur.'})
|
||||
|
||||
if not description:
|
||||
print("HATA: description eksik")
|
||||
return JsonResponse({'success': False, 'message': 'Açıklama zorunludur.'})
|
||||
|
||||
try:
|
||||
# Tutarı doğru şekilde kontrol et
|
||||
if amount is None or amount == '':
|
||||
print("HATA: amount boş")
|
||||
return JsonResponse({'success': False, 'message': 'Tutar boş olamaz.'})
|
||||
|
||||
# Virgül olan sayıları nokta ile değiştir
|
||||
if isinstance(amount, str) and ',' in amount:
|
||||
amount = amount.replace(',', '.')
|
||||
|
||||
# Decimal dönüşümü için her zaman string kullan
|
||||
amount_decimal = Decimal(str(amount))
|
||||
|
||||
if amount_decimal <= 0:
|
||||
print("HATA: amount negatif veya sıfır")
|
||||
return JsonResponse({'success': False, 'message': 'Geçerli bir tutar giriniz.'})
|
||||
|
||||
print(f"Tutar başarıyla dönüştürüldü: {amount_decimal}")
|
||||
except Exception as e:
|
||||
print(f"HATA: amount geçersiz: {str(e)}")
|
||||
return JsonResponse({'success': False, 'message': f'Geçerli bir tutar giriniz. Hata: {str(e)}'})
|
||||
|
||||
# Müşteriyi bul
|
||||
try:
|
||||
customer = Customer.objects.get(id=customer_id)
|
||||
print(f"Müşteri bulundu: {customer}")
|
||||
except Customer.DoesNotExist:
|
||||
print(f"HATA: customer_id={customer_id} için müşteri bulunamadı")
|
||||
return JsonResponse({'success': False, 'message': 'Geçersiz müşteri.'})
|
||||
|
||||
# Tarihleri doğru formata çevir
|
||||
try:
|
||||
issue_date_obj = datetime.strptime(issue_date, '%Y-%m-%d').date()
|
||||
due_date_obj = datetime.strptime(due_date, '%Y-%m-%d').date()
|
||||
print(f"Tarihler başarıyla parse edildi: {issue_date_obj}, {due_date_obj}")
|
||||
except ValueError as e:
|
||||
print(f"HATA: Tarih formatı hatalı: {str(e)}")
|
||||
return JsonResponse({'success': False, 'message': 'Geçersiz tarih formatı.'})
|
||||
|
||||
print("Fatura oluşturuluyor...")
|
||||
# Fatura oluştur
|
||||
invoice = Invoice.objects.create(
|
||||
customer=customer,
|
||||
invoice_type=invoice_type,
|
||||
issue_date=issue_date_obj,
|
||||
due_date=due_date_obj,
|
||||
payment_method=payment_method,
|
||||
status='draft'
|
||||
)
|
||||
print(f"Fatura oluşturuldu: ID={invoice.id}, Numara={invoice.invoice_number}, Tip={invoice_type}")
|
||||
|
||||
# Projeyi bul
|
||||
project = None
|
||||
if project_id:
|
||||
try:
|
||||
project = Project.objects.get(id=project_id)
|
||||
print(f"Proje bulundu: {project}")
|
||||
except Project.DoesNotExist:
|
||||
print(f"UYARI: project_id={project_id} için proje bulunamadı")
|
||||
pass
|
||||
|
||||
print("Fatura kalemi oluşturuluyor...")
|
||||
# Tek fatura kalemi olarak ekle - amount_decimal zaten oluşturuldu, tekrar dönüştürmeye gerek yok
|
||||
item = InvoiceItem.objects.create(
|
||||
invoice=invoice,
|
||||
project=project,
|
||||
description=description,
|
||||
amount=amount_decimal
|
||||
)
|
||||
print(f"Fatura kalemi oluşturuldu: ID={item.id}")
|
||||
|
||||
# Başarılı yanıt döndür
|
||||
response_data = {
|
||||
'success': True,
|
||||
'message': 'Fatura başarıyla oluşturuldu.',
|
||||
'invoice_id': invoice.id,
|
||||
'invoice_number': invoice.invoice_number
|
||||
}
|
||||
print("Başarılı yanıt:", response_data)
|
||||
return JsonResponse(response_data)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"KRİTİK HATA: Fatura oluşturulamadı: {str(e)}")
|
||||
print(traceback.format_exc())
|
||||
|
||||
# Daha detaylı hata mesajı oluştur
|
||||
error_type = type(e).__name__
|
||||
error_msg = str(e)
|
||||
|
||||
if error_type == 'InvalidOperation':
|
||||
# Decimal dönüşüm hatası
|
||||
return JsonResponse({'success': False, 'message': f'Geçersiz tutar formatı. Lütfen sayısal bir değer girin.'})
|
||||
else:
|
||||
# Diğer hatalar
|
||||
return JsonResponse({'success': False, 'message': f'Beklenmeyen bir hata oluştu: {error_type} - {error_msg}'})
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def update_invoice(request, invoice_id):
|
||||
"""Fatura güncelle"""
|
||||
try:
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
invoice = get_object_or_404(Invoice, id=invoice_id)
|
||||
data = json.loads(request.body)
|
||||
|
||||
# Temel fatura bilgilerini güncelle
|
||||
if 'issue_date' in data:
|
||||
try:
|
||||
invoice.issue_date = datetime.strptime(data['issue_date'], '%Y-%m-%d').date()
|
||||
except ValueError as e:
|
||||
return JsonResponse({'success': False, 'message': f'Geçersiz düzenleme tarihi formatı: {str(e)}'})
|
||||
|
||||
if 'due_date' in data:
|
||||
try:
|
||||
invoice.due_date = datetime.strptime(data['due_date'], '%Y-%m-%d').date()
|
||||
except ValueError as e:
|
||||
return JsonResponse({'success': False, 'message': f'Geçersiz son ödeme tarihi formatı: {str(e)}'})
|
||||
|
||||
if 'payment_method' in data:
|
||||
invoice.payment_method = data['payment_method']
|
||||
|
||||
if 'status' in data:
|
||||
invoice.status = data['status']
|
||||
|
||||
# Müşteri değişikliği
|
||||
if 'customer_id' in data:
|
||||
try:
|
||||
customer = Customer.objects.get(id=data['customer_id'])
|
||||
invoice.customer = customer
|
||||
except Customer.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'message': 'Geçersiz müşteri.'})
|
||||
|
||||
invoice.save()
|
||||
|
||||
# Fatura açıklama ve tutarını güncelle
|
||||
description = data.get('description')
|
||||
amount = data.get('amount')
|
||||
project_id = data.get('project_id')
|
||||
|
||||
# Mevcut kalemleri temizle
|
||||
invoice.items.all().delete()
|
||||
|
||||
# Projeyi bul
|
||||
project = None
|
||||
if project_id:
|
||||
try:
|
||||
project = Project.objects.get(id=project_id)
|
||||
except Project.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Tutarı güvenli bir şekilde Decimal'e dönüştür
|
||||
if description is not None and amount is not None:
|
||||
try:
|
||||
# Virgül olan sayıları nokta ile değiştir
|
||||
if isinstance(amount, str) and ',' in amount:
|
||||
amount = amount.replace(',', '.')
|
||||
|
||||
# Decimal dönüşümü için her zaman string kullan
|
||||
amount_decimal = Decimal(str(amount))
|
||||
|
||||
if amount_decimal <= 0:
|
||||
return JsonResponse({'success': False, 'message': 'Geçerli bir tutar giriniz (0\'dan büyük olmalı).'})
|
||||
|
||||
# Yeni kalemi ekle
|
||||
InvoiceItem.objects.create(
|
||||
invoice=invoice,
|
||||
project=project,
|
||||
description=description,
|
||||
amount=amount_decimal
|
||||
)
|
||||
except (InvalidOperation, ValueError, TypeError) as e:
|
||||
return JsonResponse({'success': False, 'message': f'Geçersiz tutar formatı: {str(e)}'})
|
||||
else:
|
||||
return JsonResponse({'success': False, 'message': 'Açıklama ve tutar alanları gereklidir.'})
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': 'Fatura başarıyla güncellendi.',
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_trace = traceback.format_exc()
|
||||
print(f"Fatura güncelleme hatası: {str(e)}")
|
||||
print(f"Hata ayrıntıları: {error_trace}")
|
||||
|
||||
# Decimal hatası için özel mesaj
|
||||
if 'decimal.InvalidOperation' in str(e):
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': 'Fatura tutarı geçersiz format içeriyor. Lütfen geçerli bir sayısal değer girin.'
|
||||
})
|
||||
else:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': f'Fatura güncelleme hatası: {str(e)}'
|
||||
})
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["DELETE"])
|
||||
def delete_invoice(request, invoice_id):
|
||||
"""Fatura sil"""
|
||||
try:
|
||||
invoice = get_object_or_404(Invoice, id=invoice_id)
|
||||
invoice_number = invoice.invoice_number
|
||||
invoice.delete()
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'{invoice_number} numaralı fatura silindi.'
|
||||
})
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': f'Fatura silme hatası: {str(e)}'
|
||||
})
|
||||
|
||||
@login_required
|
||||
def get_invoice_details(request, invoice_id):
|
||||
"""Fatura detay bilgilerini JSON olarak döndür - API endpoint"""
|
||||
try:
|
||||
from decimal import Decimal, InvalidOperation
|
||||
import traceback
|
||||
|
||||
print(f"get_invoice_details fonksiyonu çağrıldı: Invoice ID = {invoice_id}")
|
||||
|
||||
try:
|
||||
invoice = get_object_or_404(Invoice, id=invoice_id)
|
||||
print(f"Invoice {invoice_id} bulundu")
|
||||
except Exception as e:
|
||||
print(f"Invoice {invoice_id} bulunamadı: {str(e)}")
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': f'Fatura bulunamadı (ID: {invoice_id})'
|
||||
}, status=404)
|
||||
|
||||
# Decimal değerlerini güvenli bir şekilde dizeye dönüştür
|
||||
def safe_decimal_to_str(value):
|
||||
if value is None:
|
||||
return "0.00"
|
||||
try:
|
||||
# Eğer string ise ve virgül içeriyorsa, nokta ile değiştir
|
||||
if isinstance(value, str) and ',' in value:
|
||||
value = value.replace(',', '.')
|
||||
|
||||
# Decimal'e dönüştür
|
||||
decimal_value = Decimal(str(value))
|
||||
return str(decimal_value)
|
||||
except (InvalidOperation, ValueError, TypeError) as e:
|
||||
print(f"Decimal dönüşüm hatası: {value} - {str(e)}")
|
||||
return "0.00"
|
||||
|
||||
# Müşteri bilgisini güvenli bir şekilde al
|
||||
customer_data = {'id': 0, 'name': 'Bilinmeyen Müşteri'}
|
||||
try:
|
||||
if invoice.customer:
|
||||
customer_data = {
|
||||
'id': invoice.customer.id,
|
||||
'name': invoice.customer.name # Direkt olarak name özelliğini kullan
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Müşteri bilgisi alınamadı: {str(e)}")
|
||||
|
||||
# Temel fatura bilgilerini güvenli şekilde al
|
||||
invoice_data = {
|
||||
'id': invoice.id,
|
||||
'invoice_number': invoice.invoice_number or f"FATURA-{invoice.id}",
|
||||
'status': invoice.status or 'draft',
|
||||
'payment_method': invoice.payment_method or 'bank_transfer',
|
||||
'total_amount': safe_decimal_to_str(invoice.total_amount),
|
||||
'notes': invoice.notes or '',
|
||||
'payment_notes': invoice.payment_notes or '',
|
||||
'customer': customer_data,
|
||||
'items': []
|
||||
}
|
||||
|
||||
# Tarihleri güvenli şekilde ekle
|
||||
try:
|
||||
invoice_data['issue_date'] = invoice.issue_date.strftime('%Y-%m-%d') if invoice.issue_date else ""
|
||||
except Exception as e:
|
||||
print(f"Issue date formatlanırken hata: {str(e)}")
|
||||
invoice_data['issue_date'] = ""
|
||||
|
||||
try:
|
||||
invoice_data['due_date'] = invoice.due_date.strftime('%Y-%m-%d') if invoice.due_date else ""
|
||||
except Exception as e:
|
||||
print(f"Due date formatlanırken hata: {str(e)}")
|
||||
invoice_data['due_date'] = ""
|
||||
|
||||
try:
|
||||
invoice_data['created_at'] = invoice.created_at.strftime('%Y-%m-%d %H:%M:%S') if invoice.created_at else ""
|
||||
invoice_data['updated_at'] = invoice.updated_at.strftime('%Y-%m-%d %H:%M:%S') if invoice.updated_at else ""
|
||||
except Exception as e:
|
||||
print(f"Oluşturma/güncelleme tarihi formatlanırken hata: {str(e)}")
|
||||
invoice_data['created_at'] = ""
|
||||
invoice_data['updated_at'] = ""
|
||||
|
||||
# Fatura kalemlerini güvenli şekilde al
|
||||
try:
|
||||
items = invoice.items.all()
|
||||
print(f"{len(items)} kalem bulundu")
|
||||
|
||||
# Kalem bulunamadıysa bile en azından bir tane boş kalem ekle
|
||||
if not items.exists():
|
||||
print("Fatura kalemi bulunamadı, varsayılan boş kalem ekleniyor")
|
||||
invoice_data['items'].append({
|
||||
'id': 0,
|
||||
'description': '',
|
||||
'amount': '0.00',
|
||||
'project_id': None,
|
||||
'project_name': None
|
||||
})
|
||||
|
||||
for item in items:
|
||||
try:
|
||||
# Proje bilgisini güvenli bir şekilde al
|
||||
project_id = None
|
||||
project_name = None
|
||||
|
||||
try:
|
||||
if item.project:
|
||||
project_id = item.project.id
|
||||
project_name = item.project.name
|
||||
except Exception as proj_err:
|
||||
print(f"Proje bilgisi alınamadı: {str(proj_err)}")
|
||||
|
||||
# Kalem açıklama bilgisini mutlaka ekle
|
||||
description = item.description
|
||||
if not description or description.strip() == '':
|
||||
description = 'Açıklama yok'
|
||||
print(f"Kalem {item.id} için açıklama bulunamadı, varsayılan değer kullanıldı")
|
||||
else:
|
||||
print(f"Kalem {item.id} açıklaması: '{description}'")
|
||||
|
||||
# Tutarı güvenli şekilde al
|
||||
amount = safe_decimal_to_str(item.amount)
|
||||
print(f"Kalem {item.id} tutarı: {amount}")
|
||||
|
||||
item_data = {
|
||||
'id': item.id,
|
||||
'description': description,
|
||||
'amount': amount,
|
||||
'project_id': project_id,
|
||||
'project_name': project_name
|
||||
}
|
||||
invoice_data['items'].append(item_data)
|
||||
print(f"Kalem eklendi: {item_data}")
|
||||
except Exception as item_err:
|
||||
print(f"Kalem serileştirilirken hata: {str(item_err)}")
|
||||
# Hataya neden olan kalemi atla ve devam et
|
||||
except Exception as items_err:
|
||||
print(f"Kalemler alınırken hata: {str(items_err)}")
|
||||
|
||||
# Başarılı yanıt döndür
|
||||
print("Fatura detayları başarıyla alındı")
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'invoice': invoice_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_trace = traceback.format_exc()
|
||||
print(f"Fatura detayları alınamadı hata: {str(e)}")
|
||||
print(f"Hata ayrıntıları: {error_trace}")
|
||||
|
||||
# Özel hata mesajları
|
||||
error_message = str(e)
|
||||
|
||||
if 'decimal.InvalidOperation' in error_message:
|
||||
error_message = 'Fatura tutarı geçersiz format içeriyor.'
|
||||
elif 'DoesNotExist' in error_message:
|
||||
error_message = f'Fatura (ID: {invoice_id}) bulunamadı.'
|
||||
elif 'NoneType' in error_message:
|
||||
error_message = 'Bazı veriler eksik veya geçersiz.'
|
||||
|
||||
# Daha fazla tanısal bilgi ekle
|
||||
debug_info = f"[Hata tipi: {type(e).__name__}]"
|
||||
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': f'Fatura detayları alınamadı: {error_message} {debug_info}'
|
||||
}, status=500)
|
||||
|
||||
@login_required
|
||||
def get_projects_by_customer(request, customer_id):
|
||||
"""Müşteriye ait projeleri getir"""
|
||||
try:
|
||||
projects = Project.objects.filter(customer_id=customer_id)
|
||||
data = [{
|
||||
'id': project.id,
|
||||
'name': project.name,
|
||||
'folder_name': project.folder_name,
|
||||
'url': project.url
|
||||
} for project in projects]
|
||||
|
||||
return JsonResponse({'success': True, 'projects': data})
|
||||
except Exception as e:
|
||||
return JsonResponse({'success': False, 'message': str(e)})
|
||||
|
||||
@login_required
|
||||
def invoice_reports(request):
|
||||
"""Fatura raporları sayfası"""
|
||||
# Tarih filtreleri
|
||||
today = timezone.now().date()
|
||||
start_date = request.GET.get('start_date', (today - timedelta(days=30)).strftime('%Y-%m-%d'))
|
||||
end_date = request.GET.get('end_date', today.strftime('%Y-%m-%d'))
|
||||
|
||||
# String tarihleri datetime'a çevir
|
||||
try:
|
||||
start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
|
||||
end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
start_date = today - timedelta(days=30)
|
||||
end_date = today
|
||||
|
||||
# Filtreler
|
||||
invoices = Invoice.objects.filter(issue_date__range=[start_date, end_date])
|
||||
|
||||
# İstatistikler
|
||||
stats = {
|
||||
'total_invoices': invoices.count(),
|
||||
'total_amount': invoices.aggregate(Sum('total_amount'))['total_amount__sum'] or 0,
|
||||
'paid_amount': invoices.filter(status='paid').aggregate(Sum('total_amount'))['total_amount__sum'] or 0,
|
||||
'unpaid_amount': invoices.filter(status__in=['draft', 'sent', 'overdue']).aggregate(Sum('total_amount'))['total_amount__sum'] or 0,
|
||||
'paid_count': invoices.filter(status='paid').count(),
|
||||
'unpaid_count': invoices.filter(status__in=['draft', 'sent', 'overdue']).count(),
|
||||
'overdue_count': invoices.filter(status='overdue').count(),
|
||||
}
|
||||
|
||||
# Müşterilere göre dağılım
|
||||
customers_data = invoices.values('customer__name').annotate(
|
||||
total=Sum('total_amount')
|
||||
).order_by('-total')[:10]
|
||||
|
||||
# Durumlara göre dağılım
|
||||
status_data = invoices.values('status').annotate(
|
||||
count=models.Count('id'),
|
||||
total=Sum('total_amount')
|
||||
).order_by('status')
|
||||
|
||||
context = {
|
||||
'stats': stats,
|
||||
'customers_data': customers_data,
|
||||
'status_data': status_data,
|
||||
'start_date': start_date.strftime('%Y-%m-%d'),
|
||||
'end_date': end_date.strftime('%Y-%m-%d'),
|
||||
'today': today.strftime('%Y-%m-%d'),
|
||||
}
|
||||
|
||||
return render(request, 'ssh_manager/fatura_raporlari.html', context)
|
||||
|
||||
@login_required
|
||||
def test_fatura(request):
|
||||
"""Test fatura sayfası"""
|
||||
customers = Customer.objects.filter(is_active=True).order_by('name')
|
||||
|
||||
context = {
|
||||
'customers': customers
|
||||
}
|
||||
return render(request, 'ssh_manager/test_fatura.html', context)
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def update_invoice_status(request, invoice_id):
|
||||
"""Fatura durumunu günceller"""
|
||||
try:
|
||||
# JSON veriyi parse et
|
||||
data = json.loads(request.body)
|
||||
new_status = data.get('status')
|
||||
|
||||
if not new_status:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': 'Durum bilgisi gerekli'
|
||||
}, status=400)
|
||||
|
||||
# Geçerli durum kontrolü
|
||||
valid_statuses = ['draft', 'sent', 'paid', 'overdue', 'cancelled']
|
||||
if new_status not in valid_statuses:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': f'Geçersiz durum: {new_status}'
|
||||
}, status=400)
|
||||
|
||||
# Faturayı bul ve güncelle
|
||||
invoice = get_object_or_404(Invoice, id=invoice_id)
|
||||
old_status = invoice.status
|
||||
invoice.status = new_status
|
||||
invoice.save()
|
||||
|
||||
# Log mesajı
|
||||
print(f"Fatura {invoice_id} durumu '{old_status}' -> '{new_status}' olarak güncellendi")
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'Fatura durumu başarıyla güncellendi',
|
||||
'old_status': old_status,
|
||||
'new_status': new_status
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': 'Geçersiz JSON verisi'
|
||||
}, status=400)
|
||||
except Exception as e:
|
||||
print(f"Fatura durum güncelleme hatası: {str(e)}")
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': f'Durum güncellenirken hata oluştu: {str(e)}'
|
||||
}, status=500)
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def bulk_update_invoice_status(request):
|
||||
"""Toplu fatura durum güncelleme"""
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
invoice_ids = data.get('invoice_ids', [])
|
||||
new_status = data.get('status')
|
||||
|
||||
if not invoice_ids or not new_status:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': 'Fatura ID\'leri ve durum bilgisi gerekli'
|
||||
}, status=400)
|
||||
|
||||
# Geçerli durum kontrolü
|
||||
valid_statuses = ['draft', 'sent', 'paid', 'overdue', 'cancelled']
|
||||
if new_status not in valid_statuses:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': f'Geçersiz durum: {new_status}'
|
||||
}, status=400)
|
||||
|
||||
# Faturaları toplu güncelle
|
||||
updated_count = Invoice.objects.filter(id__in=invoice_ids).update(status=new_status)
|
||||
|
||||
print(f"{updated_count} fatura durumu '{new_status}' olarak güncellendi")
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'{updated_count} fatura durumu başarıyla güncellendi',
|
||||
'updated_count': updated_count,
|
||||
'new_status': new_status
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': 'Geçersiz JSON verisi'
|
||||
}, status=400)
|
||||
except Exception as e:
|
||||
print(f"Toplu durum güncelleme hatası: {str(e)}")
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': f'Durumlar güncellenirken hata oluştu: {str(e)}'
|
||||
}, status=500)
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["DELETE"])
|
||||
def bulk_delete_invoices(request):
|
||||
"""Toplu fatura silme"""
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
invoice_ids = data.get('invoice_ids', [])
|
||||
|
||||
if not invoice_ids:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': 'Fatura ID\'leri gerekli'
|
||||
}, status=400)
|
||||
|
||||
# Faturaları toplu sil
|
||||
deleted_count, _ = Invoice.objects.filter(id__in=invoice_ids).delete()
|
||||
|
||||
print(f"{deleted_count} fatura silindi")
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'{deleted_count} fatura başarıyla silindi',
|
||||
'deleted_count': deleted_count
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': 'Geçersiz JSON verisi'
|
||||
}, status=400)
|
||||
except Exception as e:
|
||||
print(f"Toplu silme hatası: {str(e)}")
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': f'Faturalar silinirken hata oluştu: {str(e)}'
|
||||
}, status=500)
|
||||
|
||||
@login_required
|
||||
def profit_loss_report(request):
|
||||
"""Kar/Zarar raporu"""
|
||||
# Tarih filtreleri
|
||||
today = timezone.now().date()
|
||||
start_date = request.GET.get('start_date', (today - timedelta(days=30)).strftime('%Y-%m-%d'))
|
||||
end_date = request.GET.get('end_date', today.strftime('%Y-%m-%d'))
|
||||
customer_id = request.GET.get('customer_id', '')
|
||||
|
||||
# String tarihleri datetime'a çevir
|
||||
try:
|
||||
start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
|
||||
end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
start_date = today - timedelta(days=30)
|
||||
end_date = today
|
||||
|
||||
# Temel sorgu
|
||||
invoices = Invoice.objects.filter(
|
||||
issue_date__range=[start_date, end_date],
|
||||
status='paid' # Sadece ödenmiş faturalar
|
||||
)
|
||||
|
||||
# Müşteri filtresi
|
||||
if customer_id:
|
||||
try:
|
||||
customer = Customer.objects.get(id=customer_id)
|
||||
invoices = invoices.filter(customer=customer)
|
||||
except Customer.DoesNotExist:
|
||||
customer = None
|
||||
else:
|
||||
customer = None
|
||||
|
||||
# Gelir ve gider hesaplamaları
|
||||
income_invoices = invoices.filter(invoice_type='income')
|
||||
expense_invoices = invoices.filter(invoice_type='expense')
|
||||
|
||||
total_income = income_invoices.aggregate(Sum('total_amount'))['total_amount__sum'] or 0
|
||||
total_expense = expense_invoices.aggregate(Sum('total_amount'))['total_amount__sum'] or 0
|
||||
net_profit = total_income - total_expense
|
||||
|
||||
# Genel özet
|
||||
summary = {
|
||||
'total_income': total_income,
|
||||
'total_expense': total_expense,
|
||||
'net_profit': net_profit,
|
||||
'profit_margin': (net_profit / total_income * 100) if total_income > 0 else 0,
|
||||
'income_count': income_invoices.count(),
|
||||
'expense_count': expense_invoices.count(),
|
||||
}
|
||||
|
||||
# Müşteri bazında kar/zarar (sadece genel rapor için)
|
||||
customer_stats = []
|
||||
if not customer_id:
|
||||
customers = Customer.objects.filter(
|
||||
invoices__issue_date__range=[start_date, end_date],
|
||||
invoices__status='paid'
|
||||
).distinct()
|
||||
|
||||
for cust in customers:
|
||||
cust_invoices = invoices.filter(customer=cust)
|
||||
cust_income = cust_invoices.filter(invoice_type='income').aggregate(Sum('total_amount'))['total_amount__sum'] or 0
|
||||
cust_expense = cust_invoices.filter(invoice_type='expense').aggregate(Sum('total_amount'))['total_amount__sum'] or 0
|
||||
cust_profit = cust_income - cust_expense
|
||||
|
||||
customer_stats.append({
|
||||
'customer': cust,
|
||||
'income': cust_income,
|
||||
'expense': cust_expense,
|
||||
'profit': cust_profit,
|
||||
'profit_margin': (cust_profit / cust_income * 100) if cust_income > 0 else 0,
|
||||
})
|
||||
|
||||
# Kar'a göre sırala
|
||||
customer_stats.sort(key=lambda x: x['profit'], reverse=True)
|
||||
|
||||
# Aylık trend analizi
|
||||
monthly_data = []
|
||||
current_date = start_date.replace(day=1) # Ay başına ayarla
|
||||
|
||||
while current_date <= end_date:
|
||||
month_end = current_date.replace(day=28) + timedelta(days=4) # Sonraki ay
|
||||
month_end = month_end - timedelta(days=month_end.day) # Ayın son günü
|
||||
|
||||
month_invoices = invoices.filter(
|
||||
issue_date__range=[current_date, month_end]
|
||||
)
|
||||
|
||||
month_income = month_invoices.filter(invoice_type='income').aggregate(Sum('total_amount'))['total_amount__sum'] or 0
|
||||
month_expense = month_invoices.filter(invoice_type='expense').aggregate(Sum('total_amount'))['total_amount__sum'] or 0
|
||||
|
||||
monthly_data.append({
|
||||
'month': current_date.strftime('%Y-%m'),
|
||||
'month_name': current_date.strftime('%B %Y'),
|
||||
'income': month_income,
|
||||
'expense': month_expense,
|
||||
'profit': month_income - month_expense,
|
||||
})
|
||||
|
||||
# Sonraki aya geç
|
||||
if current_date.month == 12:
|
||||
current_date = current_date.replace(year=current_date.year + 1, month=1)
|
||||
else:
|
||||
current_date = current_date.replace(month=current_date.month + 1)
|
||||
|
||||
# Son faturalar (gelir/gider ayrımı ile)
|
||||
recent_income = income_invoices.order_by('-issue_date')[:5]
|
||||
recent_expense = expense_invoices.order_by('-issue_date')[:5]
|
||||
|
||||
context = {
|
||||
'summary': summary,
|
||||
'customer_stats': customer_stats,
|
||||
'monthly_data': monthly_data,
|
||||
'recent_income': recent_income,
|
||||
'recent_expense': recent_expense,
|
||||
'start_date': start_date.strftime('%Y-%m-%d'),
|
||||
'end_date': end_date.strftime('%Y-%m-%d'),
|
||||
'today': today.strftime('%Y-%m-%d'),
|
||||
'selected_customer': customer,
|
||||
'customers': Customer.objects.filter(is_active=True).order_by('name'),
|
||||
}
|
||||
|
||||
return render(request, 'ssh_manager/kar_zarar_raporu.html', context)
|
||||
57
ssh_manager/migrations/0013_invoice_invoiceitem.py
Normal file
57
ssh_manager/migrations/0013_invoice_invoiceitem.py
Normal file
@ -0,0 +1,57 @@
|
||||
# Generated by Django 5.2.4 on 2025-07-24 19:21
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ssh_manager', '0012_sshcredential_connection_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Invoice',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('invoice_number', models.CharField(max_length=50, unique=True, verbose_name='Fatura No')),
|
||||
('issue_date', models.DateField(verbose_name='Düzenleme Tarihi')),
|
||||
('due_date', models.DateField(verbose_name='Son Ödeme Tarihi')),
|
||||
('status', models.CharField(choices=[('draft', 'Taslak'), ('sent', 'Gönderildi'), ('paid', 'Ödendi'), ('overdue', 'Gecikti'), ('cancelled', 'İptal Edildi')], default='draft', max_length=20, verbose_name='Durum')),
|
||||
('payment_method', models.CharField(choices=[('bank_transfer', 'Banka Havalesi'), ('credit_card', 'Kredi Kartı'), ('cash', 'Nakit'), ('other', 'Diğer')], default='bank_transfer', max_length=20, verbose_name='Ödeme Yöntemi')),
|
||||
('subtotal', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Ara Toplam')),
|
||||
('tax_amount', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='KDV Tutarı')),
|
||||
('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Toplam Tutar')),
|
||||
('discount', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='İndirim')),
|
||||
('notes', models.TextField(blank=True, null=True, verbose_name='Notlar')),
|
||||
('payment_notes', models.TextField(blank=True, null=True, verbose_name='Ödeme Notları')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Oluşturma Tarihi')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi')),
|
||||
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to='ssh_manager.customer', verbose_name='Müşteri')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Fatura',
|
||||
'verbose_name_plural': 'Faturalar',
|
||||
'ordering': ['-issue_date', '-id'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InvoiceItem',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('description', models.CharField(max_length=255, verbose_name='Açıklama')),
|
||||
('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=10, verbose_name='Miktar')),
|
||||
('unit_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Birim Fiyat')),
|
||||
('tax_rate', models.DecimalField(decimal_places=2, default=18, max_digits=5, verbose_name='KDV Oranı (%)')),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Tutar')),
|
||||
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='ssh_manager.invoice', verbose_name='Fatura')),
|
||||
('project', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ssh_manager.project', verbose_name='İlişkili Proje')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Fatura Kalemi',
|
||||
'verbose_name_plural': 'Fatura Kalemleri',
|
||||
'ordering': ['id'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,37 @@
|
||||
# Generated by Django 5.2.4 on 2025-07-24 20:51
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ssh_manager', '0013_invoice_invoiceitem'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='invoice',
|
||||
name='discount',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='invoice',
|
||||
name='subtotal',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='invoice',
|
||||
name='tax_amount',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='invoiceitem',
|
||||
name='quantity',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='invoiceitem',
|
||||
name='tax_rate',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='invoiceitem',
|
||||
name='unit_price',
|
||||
),
|
||||
]
|
||||
34
ssh_manager/migrations/0015_backup.py
Normal file
34
ssh_manager/migrations/0015_backup.py
Normal file
@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.2.4 on 2025-07-26 03:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ssh_manager', '0014_remove_invoice_discount_remove_invoice_subtotal_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Backup',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('backup_type', models.CharField(choices=[('manual', 'Manuel'), ('auto', 'Otomatik'), ('scheduled', 'Zamanlanmış')], default='manual', max_length=20, verbose_name='Yedekleme Tipi')),
|
||||
('status', models.CharField(choices=[('running', 'Devam Ediyor'), ('completed', 'Tamamlandı'), ('failed', 'Başarısız'), ('cancelled', 'İptal Edildi')], default='running', max_length=20, verbose_name='Durum')),
|
||||
('start_time', models.DateTimeField(auto_now_add=True, verbose_name='Başlangıç Zamanı')),
|
||||
('end_time', models.DateTimeField(blank=True, null=True, verbose_name='Bitiş Zamanı')),
|
||||
('file_path', models.CharField(blank=True, max_length=500, null=True, verbose_name='Dosya Yolu')),
|
||||
('file_size', models.CharField(blank=True, max_length=50, null=True, verbose_name='Dosya Boyutu')),
|
||||
('error_message', models.TextField(blank=True, null=True, verbose_name='Hata Mesajı')),
|
||||
('notes', models.TextField(blank=True, null=True, verbose_name='Notlar')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ssh_manager.project', verbose_name='Proje')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Yedekleme',
|
||||
'verbose_name_plural': 'Yedeklemeler',
|
||||
'ordering': ['-start_time'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,49 @@
|
||||
# Generated by Django 5.2.4 on 2025-07-26 08:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ssh_manager', '0015_backup'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SystemSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('backup_enabled', models.BooleanField(default=False, verbose_name='Otomatik Yedekleme Aktif')),
|
||||
('backup_frequency', models.CharField(choices=[('daily', 'Günlük'), ('weekly', 'Haftalık'), ('monthly', 'Aylık')], default='weekly', max_length=10, verbose_name='Yedekleme Sıklığı')),
|
||||
('backup_hour', models.IntegerField(default=3, verbose_name='Yedekleme Saati (0-23)')),
|
||||
('backup_minute', models.IntegerField(default=0, verbose_name='Yedekleme Dakikası (0-59)')),
|
||||
('backup_day_of_week', models.IntegerField(blank=True, default=0, null=True, verbose_name='Haftanın Günü (0=Pazartesi, 6=Pazar)')),
|
||||
('backup_day_of_month', models.IntegerField(blank=True, default=1, null=True, verbose_name='Ayın Günü (1-31)')),
|
||||
('backup_retention_days', models.IntegerField(default=30, verbose_name='Yedekleri Saklama Süresi (Gün)')),
|
||||
('backup_crontab_expression', models.CharField(blank=True, max_length=100, null=True, verbose_name='Crontab İfadesi')),
|
||||
('backup_compression', models.BooleanField(default=True, verbose_name='Sıkıştırma Kullan')),
|
||||
('backup_format', models.CharField(default='tar.gz', max_length=10, verbose_name='Yedekleme Format')),
|
||||
('ssh_key_path', models.CharField(blank=True, max_length=255, null=True, verbose_name='SSH Anahtar Yolu')),
|
||||
('ssh_key_passphrase', models.CharField(blank=True, max_length=255, null=True, verbose_name='SSH Anahtar Parolası')),
|
||||
('email_notifications', models.BooleanField(default=False, verbose_name='E-posta Bildirimleri')),
|
||||
('notification_email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Bildirim E-postası')),
|
||||
('backup_directory', models.CharField(default='/backups', max_length=255, verbose_name='Yedekleme Dizini')),
|
||||
('use_s3_storage', models.BooleanField(default=False, verbose_name='S3 Depolama Kullan')),
|
||||
('s3_access_key', models.CharField(blank=True, max_length=255, null=True, verbose_name='S3 Erişim Anahtarı')),
|
||||
('s3_secret_key', models.CharField(blank=True, max_length=255, null=True, verbose_name='S3 Gizli Anahtar')),
|
||||
('s3_bucket_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='S3 Bucket Adı')),
|
||||
('s3_region', models.CharField(blank=True, max_length=50, null=True, verbose_name='S3 Bölge')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Sistem Ayarları',
|
||||
'verbose_name_plural': 'Sistem Ayarları',
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='backup',
|
||||
name='file_size',
|
||||
field=models.BigIntegerField(blank=True, null=True, verbose_name='Dosya Boyutu'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,55 @@
|
||||
# Generated by Django 5.2.4 on 2025-07-26 09:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ssh_manager', '0016_systemsettings_alter_backup_file_size'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='systemsettings',
|
||||
name='backup_directory',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='systemsettings',
|
||||
name='ssh_key_passphrase',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='systemsettings',
|
||||
name='ssh_key_path',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemsettings',
|
||||
name='s3_endpoint',
|
||||
field=models.CharField(default='ams1.vultrobjects.com', max_length=255, verbose_name='Vultr S3 Endpoint'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemsettings',
|
||||
name='s3_access_key',
|
||||
field=models.CharField(default='', max_length=255, verbose_name='Vultr S3 Erişim Anahtarı'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemsettings',
|
||||
name='s3_bucket_name',
|
||||
field=models.CharField(default='backups', max_length=255, verbose_name='Vultr S3 Bucket Adı'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemsettings',
|
||||
name='s3_region',
|
||||
field=models.CharField(default='ams', max_length=50, verbose_name='Vultr S3 Bölge'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemsettings',
|
||||
name='s3_secret_key',
|
||||
field=models.CharField(default='', max_length=255, verbose_name='Vultr S3 Gizli Anahtar'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemsettings',
|
||||
name='use_s3_storage',
|
||||
field=models.BooleanField(default=True, help_text='Tüm yedekler Vultr S3 depolama alanına aktarılır', verbose_name='Vultr S3 Depolama'),
|
||||
),
|
||||
]
|
||||
@ -3,6 +3,7 @@ import logging
|
||||
from .ssh_client import SSHManager
|
||||
from django.core.validators import URLValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
from .system_settings import SystemSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -104,9 +105,10 @@ class Project(models.Model):
|
||||
folder_name = models.CharField(max_length=100, verbose_name='Klasör Adı')
|
||||
ssh_credential = models.ForeignKey(SSHCredential, on_delete=models.CASCADE)
|
||||
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, verbose_name='Müşteri', null=True, blank=True)
|
||||
url = models.CharField(max_length=255, null=True, blank=True)
|
||||
url = models.TextField(null=True, blank=True, verbose_name='Site URL')
|
||||
disk_usage = models.CharField(max_length=20, null=True, blank=True)
|
||||
last_backup = models.DateTimeField(null=True, blank=True, verbose_name='Son Yedekleme')
|
||||
host_renewal_date = models.DateField(null=True, blank=True, verbose_name='Host Yenileme Tarihi')
|
||||
meta_key = models.CharField(max_length=32, null=True, blank=True, verbose_name='Meta Key', help_text='Site aktiflik kontrolü için benzersiz anahtar')
|
||||
is_site_active = models.BooleanField(default=False, verbose_name='Site Aktif')
|
||||
last_site_check = models.DateTimeField(null=True, blank=True, verbose_name='Son Site Kontrolü')
|
||||
@ -124,19 +126,21 @@ class Project(models.Model):
|
||||
if not self.meta_key:
|
||||
self.generate_meta_key()
|
||||
self.save()
|
||||
return f'<meta name="site-verification" content="{self.meta_key}">'
|
||||
|
||||
# Farklı meta tag formatları
|
||||
meta_tags = [
|
||||
f'<meta name="site-verification" content="{self.meta_key}">',
|
||||
f'<meta name="site-verify" content="{self.meta_key}">',
|
||||
f'<meta name="verification" content="{self.meta_key}">'
|
||||
]
|
||||
|
||||
# HTML yorum içinde meta key
|
||||
comment_tag = f'<!-- site-verification: {self.meta_key} -->'
|
||||
|
||||
# Tüm formatları birleştir
|
||||
return meta_tags[0], meta_tags, comment_tag
|
||||
|
||||
def clean(self):
|
||||
# URL formatını kontrol et
|
||||
if self.url:
|
||||
# URL'den http/https ve www. kısımlarını temizle
|
||||
cleaned_url = self.url.lower()
|
||||
for prefix in ['http://', 'https://', 'www.']:
|
||||
if cleaned_url.startswith(prefix):
|
||||
cleaned_url = cleaned_url[len(prefix):]
|
||||
# Sondaki / işaretini kaldır
|
||||
cleaned_url = cleaned_url.rstrip('/')
|
||||
self.url = cleaned_url
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.clean()
|
||||
@ -175,6 +179,131 @@ class Project(models.Model):
|
||||
verbose_name_plural = "Projeler"
|
||||
ordering = ['-created_at']
|
||||
|
||||
class Invoice(models.Model):
|
||||
INVOICE_STATUS = (
|
||||
('draft', 'Taslak'),
|
||||
('sent', 'Gönderildi'),
|
||||
('paid', 'Ödendi'),
|
||||
('overdue', 'Gecikti'),
|
||||
('cancelled', 'İptal Edildi'),
|
||||
)
|
||||
|
||||
PAYMENT_METHODS = (
|
||||
('bank_transfer', 'Banka Havalesi'),
|
||||
('credit_card', 'Kredi Kartı'),
|
||||
('cash', 'Nakit'),
|
||||
('other', 'Diğer'),
|
||||
)
|
||||
|
||||
INVOICE_TYPES = (
|
||||
('income', 'Gelir'),
|
||||
('expense', 'Gider'),
|
||||
)
|
||||
|
||||
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='invoices', verbose_name='Müşteri')
|
||||
invoice_number = models.CharField(max_length=50, unique=True, verbose_name='Fatura No')
|
||||
invoice_type = models.CharField(max_length=10, choices=INVOICE_TYPES, default='income', verbose_name='Fatura Tipi')
|
||||
issue_date = models.DateField(verbose_name='Düzenleme Tarihi')
|
||||
due_date = models.DateField(verbose_name='Son Ödeme Tarihi')
|
||||
status = models.CharField(max_length=20, choices=INVOICE_STATUS, default='draft', verbose_name='Durum')
|
||||
payment_method = models.CharField(max_length=20, choices=PAYMENT_METHODS, default='bank_transfer', verbose_name='Ödeme Yöntemi')
|
||||
total_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='Toplam Tutar')
|
||||
notes = models.TextField(blank=True, null=True, verbose_name='Notlar')
|
||||
payment_notes = models.TextField(blank=True, null=True, verbose_name='Ödeme Notları')
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Oluşturma Tarihi')
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi')
|
||||
|
||||
def __str__(self):
|
||||
return f"Fatura #{self.invoice_number} - {self.customer.get_display_name()}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.invoice_number:
|
||||
# Otomatik fatura numarası oluştur (F-YIL-AYGUN-XXXX)
|
||||
from datetime import datetime
|
||||
year = datetime.now().strftime('%Y')
|
||||
day_month = datetime.now().strftime('%d%m')
|
||||
|
||||
# Son fatura numarasını bul
|
||||
last_invoice = Invoice.objects.filter(
|
||||
invoice_number__startswith=f'F-{year}'
|
||||
).order_by('-invoice_number').first()
|
||||
|
||||
if last_invoice:
|
||||
try:
|
||||
last_num = int(last_invoice.invoice_number.split('-')[-1])
|
||||
new_num = last_num + 1
|
||||
except (ValueError, IndexError):
|
||||
new_num = 1
|
||||
else:
|
||||
new_num = 1
|
||||
|
||||
self.invoice_number = f'F-{year}-{day_month}-{new_num:04d}'
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Fatura"
|
||||
verbose_name_plural = "Faturalar"
|
||||
ordering = ['-issue_date', '-id']
|
||||
|
||||
class InvoiceItem(models.Model):
|
||||
invoice = models.ForeignKey(Invoice, related_name='items', on_delete=models.CASCADE, verbose_name='Fatura')
|
||||
project = models.ForeignKey(Project, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='İlişkili Proje')
|
||||
description = models.CharField(max_length=255, verbose_name='Açıklama')
|
||||
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='Tutar')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.description} - {self.invoice.invoice_number}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
self.update_invoice_total()
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
invoice = self.invoice
|
||||
super().delete(*args, **kwargs)
|
||||
# Fatura silindiğinde tutarı güncelle
|
||||
self.update_invoice_total()
|
||||
|
||||
def update_invoice_total(self):
|
||||
"""Fatura toplam tutarını güncelle"""
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
invoice = self.invoice
|
||||
items = invoice.items.all()
|
||||
|
||||
# Toplam tutar - güvenli dönüşümle hesapla
|
||||
total = Decimal('0.00')
|
||||
|
||||
for item in items:
|
||||
try:
|
||||
# Eğer item.amount None ise veya geçersizse, 0 olarak kabul et
|
||||
if item.amount is None:
|
||||
continue
|
||||
|
||||
# String ise decimal'e çevir
|
||||
if isinstance(item.amount, str):
|
||||
if ',' in item.amount:
|
||||
item.amount = item.amount.replace(',', '.')
|
||||
item_amount = Decimal(item.amount)
|
||||
else:
|
||||
item_amount = Decimal(str(item.amount))
|
||||
|
||||
total += item_amount
|
||||
except (InvalidOperation, ValueError, TypeError) as e:
|
||||
print(f"Invoice total hesaplama hatası: {str(e)}")
|
||||
# Hatalı öğeyi atla
|
||||
continue
|
||||
|
||||
# Fatura tutarını güncelle
|
||||
invoice.total_amount = total
|
||||
invoice.save()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Fatura Kalemi"
|
||||
verbose_name_plural = "Fatura Kalemleri"
|
||||
ordering = ['id']
|
||||
|
||||
class SSHLog(models.Model):
|
||||
LOG_TYPES = (
|
||||
('connection', 'Bağlantı Kontrolü'),
|
||||
@ -195,4 +324,65 @@ class SSHLog(models.Model):
|
||||
return f"{self.ssh_credential.hostname} - {self.log_type} - {self.created_at}"
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
ordering = ['-created_at']
|
||||
|
||||
class Backup(models.Model):
|
||||
BACKUP_STATUS = (
|
||||
('running', 'Devam Ediyor'),
|
||||
('completed', 'Tamamlandı'),
|
||||
('failed', 'Başarısız'),
|
||||
('cancelled', 'İptal Edildi'),
|
||||
)
|
||||
|
||||
BACKUP_TYPE = (
|
||||
('manual', 'Manuel'),
|
||||
('auto', 'Otomatik'),
|
||||
('scheduled', 'Zamanlanmış'),
|
||||
)
|
||||
|
||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, verbose_name='Proje')
|
||||
backup_type = models.CharField(max_length=20, choices=BACKUP_TYPE, default='manual', verbose_name='Yedekleme Tipi')
|
||||
status = models.CharField(max_length=20, choices=BACKUP_STATUS, default='running', verbose_name='Durum')
|
||||
start_time = models.DateTimeField(auto_now_add=True, verbose_name='Başlangıç Zamanı')
|
||||
end_time = models.DateTimeField(null=True, blank=True, verbose_name='Bitiş Zamanı')
|
||||
file_path = models.CharField(max_length=500, null=True, blank=True, verbose_name='Dosya Yolu')
|
||||
file_size = models.BigIntegerField(null=True, blank=True, verbose_name='Dosya Boyutu') # BigIntegerField kullanarak büyük dosya boyutlarını destekle
|
||||
error_message = models.TextField(null=True, blank=True, verbose_name='Hata Mesajı')
|
||||
notes = models.TextField(null=True, blank=True, verbose_name='Notlar')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.project.name} - {self.start_time.strftime('%d.%m.%Y %H:%M')}"
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
"""Yedekleme süresini hesapla"""
|
||||
if self.end_time:
|
||||
duration = self.end_time - self.start_time
|
||||
total_seconds = int(duration.total_seconds())
|
||||
hours = total_seconds // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
seconds = total_seconds % 60
|
||||
|
||||
if hours > 0:
|
||||
return f"{hours}s {minutes}d {seconds}s"
|
||||
elif minutes > 0:
|
||||
return f"{minutes}d {seconds}s"
|
||||
else:
|
||||
return f"{seconds}s"
|
||||
return "-"
|
||||
|
||||
@property
|
||||
def status_badge_class(self):
|
||||
"""Durum badge CSS sınıfı"""
|
||||
status_classes = {
|
||||
'running': 'bg-info',
|
||||
'completed': 'bg-success',
|
||||
'failed': 'bg-danger',
|
||||
'cancelled': 'bg-warning'
|
||||
}
|
||||
return status_classes.get(self.status, 'bg-secondary')
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Yedekleme"
|
||||
verbose_name_plural = "Yedeklemeler"
|
||||
ordering = ['-start_time']
|
||||
@ -16,21 +16,39 @@ class SSHManager:
|
||||
|
||||
def connect(self):
|
||||
"""SSH bağlantısı kur"""
|
||||
import traceback
|
||||
|
||||
try:
|
||||
logger.info(f"SSH bağlantısı başlatılıyor: {self.ssh_credential.hostname}:{self.ssh_credential.port or 22}")
|
||||
|
||||
self.client = paramiko.SSHClient()
|
||||
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
# Bağlantı parametrelerini logla
|
||||
logger.info(f"Bağlantı parametreleri:")
|
||||
logger.info(f" Host: {self.ssh_credential.hostname}")
|
||||
logger.info(f" Port: {self.ssh_credential.port or 22}")
|
||||
logger.info(f" Username: {self.ssh_credential.username}")
|
||||
|
||||
# SSH timeout değeri ekle
|
||||
self.client.connect(
|
||||
hostname=self.ssh_credential.hostname,
|
||||
username=self.ssh_credential.username,
|
||||
password=self.ssh_credential.password,
|
||||
port=self.ssh_credential.port or 22,
|
||||
look_for_keys=False,
|
||||
allow_agent=False
|
||||
allow_agent=False,
|
||||
timeout=30 # Timeout değeri ekle
|
||||
)
|
||||
|
||||
logger.info(f"SSH bağlantısı başarıyla kuruldu: {self.ssh_credential.hostname}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
error_details = traceback.format_exc()
|
||||
logger.error(f'SSH bağlantı hatası: {str(e)}')
|
||||
return False
|
||||
logger.error(f'SSH bağlantı hata detayları:\n{error_details}')
|
||||
raise Exception(f"SSH bağlantısı kurulamadı: {str(e)}")
|
||||
|
||||
def close(self):
|
||||
"""SSH bağlantısını kapat"""
|
||||
@ -52,12 +70,33 @@ class SSHManager:
|
||||
"""
|
||||
SSH üzerinden komut çalıştır ve sonuçları döndür
|
||||
"""
|
||||
import traceback
|
||||
|
||||
try:
|
||||
if not self.client:
|
||||
self.connect()
|
||||
logger.info(f"SSH komutu çalıştırılıyor: {command}")
|
||||
|
||||
stdin, stdout, stderr = self.client.exec_command(command)
|
||||
if not self.client:
|
||||
logger.info("SSH istemcisi yok, yeniden bağlanılıyor...")
|
||||
self.connect()
|
||||
|
||||
if not self.client:
|
||||
raise Exception("SSH istemcisi oluşturulamadı")
|
||||
|
||||
logger.info("Komut çalıştırılıyor...")
|
||||
stdin, stdout, stderr = self.client.exec_command(command, timeout=120) # Timeout ekle
|
||||
logger.info("Komut çalıştırıldı, çıkış kodu bekleniyor...")
|
||||
|
||||
# Timeout ile çıkış kodu bekle
|
||||
import select
|
||||
channel = stdout.channel
|
||||
status_ready = select.select([channel], [], [], 120) # 120 saniye timeout
|
||||
|
||||
if not status_ready[0]:
|
||||
logger.error("Komut zaman aşımına uğradı!")
|
||||
raise Exception("Komut zaman aşımına uğradı")
|
||||
|
||||
exit_status = stdout.channel.recv_exit_status()
|
||||
logger.info(f"Komut çıkış kodu: {exit_status}")
|
||||
|
||||
# Binary veriyi oku
|
||||
stdout_data = stdout.read()
|
||||
|
||||
74
ssh_manager/system_settings.py
Normal file
74
ssh_manager/system_settings.py
Normal file
@ -0,0 +1,74 @@
|
||||
from django.db import models
|
||||
|
||||
class SystemSettings(models.Model):
|
||||
"""Sistem genel ayarları"""
|
||||
BACKUP_FREQUENCY_CHOICES = [
|
||||
('daily', 'Günlük'),
|
||||
('weekly', 'Haftalık'),
|
||||
('monthly', 'Aylık'),
|
||||
]
|
||||
|
||||
# Yedekleme ayarları
|
||||
backup_enabled = models.BooleanField(default=False, verbose_name="Otomatik Yedekleme Aktif")
|
||||
backup_frequency = models.CharField(max_length=10, choices=BACKUP_FREQUENCY_CHOICES, default='weekly', verbose_name="Yedekleme Sıklığı")
|
||||
backup_hour = models.IntegerField(default=3, verbose_name="Yedekleme Saati (0-23)")
|
||||
backup_minute = models.IntegerField(default=0, verbose_name="Yedekleme Dakikası (0-59)")
|
||||
backup_day_of_week = models.IntegerField(default=0, verbose_name="Haftanın Günü (0=Pazartesi, 6=Pazar)", null=True, blank=True)
|
||||
backup_day_of_month = models.IntegerField(default=1, verbose_name="Ayın Günü (1-31)", null=True, blank=True)
|
||||
|
||||
# Linux crontab komutu
|
||||
backup_crontab_expression = models.CharField(max_length=100, blank=True, null=True, verbose_name="Crontab İfadesi")
|
||||
|
||||
# Yedekleme dosya formatı - Her zaman sıkıştırılmış
|
||||
backup_format = models.CharField(max_length=10, default='tar.gz', verbose_name="Yedekleme Format")
|
||||
|
||||
# E-posta bildirim ayarları
|
||||
email_notifications = models.BooleanField(default=False, verbose_name="E-posta Bildirimleri")
|
||||
notification_email = models.EmailField(blank=True, null=True, verbose_name="Bildirim E-postası")
|
||||
|
||||
# S3 depolama ayarları (Vultr Object Storage)
|
||||
use_s3_storage = models.BooleanField(default=True, verbose_name="Vultr S3 Depolama", help_text="Tüm yedekler Vultr S3 depolama alanına aktarılır")
|
||||
s3_access_key = models.CharField(max_length=255, default='', verbose_name="Vultr S3 Erişim Anahtarı")
|
||||
s3_secret_key = models.CharField(max_length=255, default='', verbose_name="Vultr S3 Gizli Anahtar")
|
||||
s3_bucket_name = models.CharField(max_length=255, default='backups', verbose_name="Vultr S3 Bucket Adı")
|
||||
s3_region = models.CharField(max_length=50, default='ams', verbose_name="Vultr S3 Bölge")
|
||||
s3_endpoint = models.CharField(max_length=255, default='ams1.vultrobjects.com', verbose_name="Vultr S3 Endpoint")
|
||||
|
||||
# Son güncelleme zamanı
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return "Sistem Ayarları"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Crontab ifadesini oluştur
|
||||
minute = self.backup_minute
|
||||
hour = self.backup_hour
|
||||
|
||||
if self.backup_frequency == 'daily':
|
||||
day_of_month = '*'
|
||||
month = '*'
|
||||
day_of_week = '*'
|
||||
elif self.backup_frequency == 'weekly':
|
||||
day_of_month = '*'
|
||||
month = '*'
|
||||
day_of_week = self.backup_day_of_week
|
||||
else: # monthly
|
||||
day_of_month = self.backup_day_of_month
|
||||
month = '*'
|
||||
day_of_week = '*'
|
||||
|
||||
self.backup_crontab_expression = f"{minute} {hour} {day_of_month} {month} {day_of_week}"
|
||||
|
||||
# S3 yedekleme zorunlu olduğu için her zaman kontrol et
|
||||
if not self.s3_access_key or not self.s3_secret_key or not self.s3_bucket_name:
|
||||
raise ValueError("Vultr S3 Erişim Anahtarı, Gizli Anahtar ve Bucket Adı alanları zorunludur!")
|
||||
|
||||
# S3 storage her zaman aktif
|
||||
self.use_s3_storage = True
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Sistem Ayarları"
|
||||
verbose_name_plural = "Sistem Ayarları"
|
||||
@ -1,5 +1,6 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
from . import invoice_views
|
||||
from django.apps import apps
|
||||
from django.core.management import call_command
|
||||
|
||||
@ -29,11 +30,12 @@ urlpatterns = [
|
||||
path('get_host/<int:host_id>/', views.get_host, name='get_host'),
|
||||
path('update_host/<int:host_id>/', views.update_host, name='update_host'),
|
||||
path('delete_host/<int:host_id>/', views.delete_host, name='delete_host'),
|
||||
path('update-hosts-status/', views.update_hosts_status, name='update_hosts_status'),
|
||||
path('update-hosts-status/', views.update_all_hosts_status, name='update_hosts_status'),
|
||||
path('project/create/', views.create_project, name='create_project'),
|
||||
path('project/<int:project_id>/upload/', views.upload_project_zip, name='upload_project_zip'),
|
||||
path('delete_project/<int:project_id>/', views.delete_project, name='delete_project'),
|
||||
path('project/<int:project_id>/setup-venv/', views.setup_venv, name='setup_venv'),
|
||||
path('project/<int:project_id>/renew-host/', views.renew_host, name='renew_host'),
|
||||
path('project/<int:project_id>/check-requirements/', views.check_requirements, name='check_requirements'),
|
||||
path('project/<int:project_id>/update-requirements/', views.update_requirements, name='update_requirements'),
|
||||
path('project/<int:project_id>/delete-requirement-line/', views.delete_requirement_line, name='delete_requirement_line'),
|
||||
@ -57,7 +59,7 @@ urlpatterns = [
|
||||
path('project/<int:project_id>/clear-logs/', views.clear_project_logs, name='clear_project_logs'),
|
||||
path('project/<int:project_id>/check-site/', views.check_site_status_view, name='check_site_status'),
|
||||
path('project/<int:project_id>/meta-key/', views.get_project_meta_key, name='get_project_meta_key'),
|
||||
path('check-all-sites/', views.check_all_sites_view, name='check_all_sites'),
|
||||
path('check-all-sites/', views.check_site_status_view, name='check_all_sites'),
|
||||
path('get-project-details/<int:project_id>/', views.get_project_details, name='get_project_details'),
|
||||
path('update-project/<int:project_id>/', views.update_project, name='update_project'),
|
||||
|
||||
@ -74,6 +76,23 @@ urlpatterns = [
|
||||
path('start-backup/', views.start_backup, name='start_backup'),
|
||||
path('backup-all-projects/', views.backup_all_projects, name='backup_all_projects'),
|
||||
path('retry-backup/', views.retry_backup, name='retry_backup'),
|
||||
path('backup-details/<int:backup_id>/', views.backup_details, name='backup_details'),
|
||||
path('cancel-backup/<int:backup_id>/', views.cancel_backup, name='cancel_backup'),
|
||||
path('delete-backup-record/<int:backup_id>/', views.delete_backup_record, name='delete_backup_record'),
|
||||
|
||||
# path('upload-to-drive/<int:project_id>/', views.upload_to_drive, name='upload_to_drive').
|
||||
# Invoice URLs
|
||||
path('faturalar/', invoice_views.invoices, name='faturalar'),
|
||||
path('test-fatura/', invoice_views.test_fatura, name='test_fatura'),
|
||||
path('faturalar/detay/<int:invoice_id>/', invoice_views.invoice_detail, name='invoice_detail'),
|
||||
path('faturalar/create/', invoice_views.create_invoice, name='create_invoice'),
|
||||
path('faturalar/update/<int:invoice_id>/', invoice_views.update_invoice, name='update_invoice'),
|
||||
path('faturalar/delete/<int:invoice_id>/', invoice_views.delete_invoice, name='delete_invoice'),
|
||||
path('faturalar/update-status/<int:invoice_id>/', invoice_views.update_invoice_status, name='update_invoice_status'),
|
||||
path('faturalar/bulk-update-status/', invoice_views.bulk_update_invoice_status, name='bulk_update_invoice_status'),
|
||||
path('faturalar/bulk-delete/', invoice_views.bulk_delete_invoices, name='bulk_delete_invoices'),
|
||||
path('faturalar/detay-json/<int:invoice_id>/', invoice_views.get_invoice_details, name='get_invoice_details'),
|
||||
path('faturalar/raporlar/', invoice_views.invoice_reports, name='invoice_reports'),
|
||||
path('faturalar/kar-zarar/', invoice_views.profit_loss_report, name='profit_loss_report'),
|
||||
path('musteri/<int:customer_id>/projeler/', invoice_views.get_projects_by_customer, name='get_projects_by_customer'),
|
||||
path('check-domain-expiration/', views.get_domain_expiration, name='check_domain_expiration'), # Domain sorgulama endpoint'i
|
||||
]
|
||||
|
||||
@ -10,76 +10,95 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Yardımcı fonksiyonlar buraya gelebilir
|
||||
|
||||
def check_site_status(project):
|
||||
def check_disk_usage(project):
|
||||
"""
|
||||
Projenin web sitesinin aktif olup olmadığını kontrol eder
|
||||
Aynı zamanda proje klasörünün disk kullanımını günceller
|
||||
Proje klasörünün disk kullanımını kontrol eder ve günceller
|
||||
"""
|
||||
from .ssh_client import SSHManager
|
||||
|
||||
result_messages = []
|
||||
|
||||
# 1. Disk kullanımını güncelle
|
||||
try:
|
||||
if project.ssh_credential:
|
||||
ssh_manager = SSHManager(project.ssh_credential)
|
||||
if not project.ssh_credential:
|
||||
return False, "SSH bilgisi eksik"
|
||||
|
||||
if ssh_manager.check_connection():
|
||||
# Proje klasörünün tam yolu
|
||||
base_path = project.ssh_credential.base_path.rstrip('/')
|
||||
folder_name = project.folder_name.strip('/')
|
||||
full_path = f"{base_path}/{folder_name}"
|
||||
|
||||
# Debug bilgisi ekle
|
||||
result_messages.append(f"Kontrol edilen path: {full_path}")
|
||||
|
||||
# Önce base path'in var olup olmadığını kontrol et
|
||||
base_check_command = f"test -d '{base_path}' && echo 'BASE_EXISTS' || echo 'BASE_NOT_EXISTS'"
|
||||
stdout_base, stderr_base, success_base = ssh_manager.execute_command(base_check_command)
|
||||
|
||||
if success_base and stdout_base.strip() == 'BASE_EXISTS':
|
||||
# Base path var, şimdi proje klasörünü kontrol et
|
||||
|
||||
# Önce base path içindeki klasörleri listele
|
||||
list_command = f"ls -la '{base_path}' | grep '^d'"
|
||||
stdout_list, stderr_list, success_list = ssh_manager.execute_command(list_command)
|
||||
|
||||
if success_list:
|
||||
result_messages.append(f"Base path içindeki klasörler: {stdout_list.strip()[:200]}")
|
||||
|
||||
# Proje klasörünü kontrol et
|
||||
check_command = f"test -d '{full_path}' && echo 'EXISTS' || echo 'NOT_EXISTS'"
|
||||
stdout_check, stderr_check, success_check = ssh_manager.execute_command(check_command)
|
||||
|
||||
if success_check and stdout_check.strip() == 'EXISTS':
|
||||
# Disk kullanımını al
|
||||
command = f"du -sh '{full_path}' 2>/dev/null | cut -f1"
|
||||
stdout, stderr, success = ssh_manager.execute_command(command)
|
||||
|
||||
if success and stdout.strip():
|
||||
old_usage = project.disk_usage or "Bilinmiyor"
|
||||
project.disk_usage = stdout.strip()
|
||||
result_messages.append(f"Disk kullanımı güncellendi: {old_usage} → {project.disk_usage}")
|
||||
else:
|
||||
result_messages.append("Disk kullanımı komutu başarısız")
|
||||
else:
|
||||
result_messages.append(f"Proje klasörü bulunamadı: {full_path}")
|
||||
else:
|
||||
result_messages.append(f"Base path bulunamadı: {base_path}")
|
||||
|
||||
ssh_manager.close()
|
||||
else:
|
||||
result_messages.append("SSH bağlantısı kurulamadı")
|
||||
ssh_manager = SSHManager(project.ssh_credential)
|
||||
|
||||
if not ssh_manager.check_connection():
|
||||
return False, "SSH bağlantısı kurulamadı"
|
||||
|
||||
# Proje klasörünün tam yolu
|
||||
base_path = project.ssh_credential.base_path.rstrip('/')
|
||||
folder_name = project.folder_name.strip('/')
|
||||
full_path = f"{base_path}/{folder_name}"
|
||||
|
||||
# Debug bilgisi ekle
|
||||
result_messages.append(f"Kontrol edilen path: {full_path}")
|
||||
|
||||
# Önce base path'in var olup olmadığını kontrol et
|
||||
base_check_command = f"test -d '{base_path}' && echo 'BASE_EXISTS' || echo 'BASE_NOT_EXISTS'"
|
||||
stdout_base, stderr_base, success_base = ssh_manager.execute_command(base_check_command)
|
||||
|
||||
if not (success_base and stdout_base.strip() == 'BASE_EXISTS'):
|
||||
ssh_manager.close()
|
||||
return False, f"Base path bulunamadı: {base_path}"
|
||||
|
||||
# Base path var, şimdi proje klasörünü kontrol et
|
||||
|
||||
# Önce base path içindeki klasörleri listele
|
||||
list_command = f"ls -la '{base_path}' | grep '^d'"
|
||||
stdout_list, stderr_list, success_list = ssh_manager.execute_command(list_command)
|
||||
|
||||
if success_list:
|
||||
result_messages.append(f"Base path içindeki klasörler: {stdout_list.strip()[:200]}")
|
||||
|
||||
# Proje klasörünü kontrol et
|
||||
check_command = f"test -d '{full_path}' && echo 'EXISTS' || echo 'NOT_EXISTS'"
|
||||
stdout_check, stderr_check, success_check = ssh_manager.execute_command(check_command)
|
||||
|
||||
if not (success_check and stdout_check.strip() == 'EXISTS'):
|
||||
ssh_manager.close()
|
||||
return False, f"Proje klasörü bulunamadı: {full_path}"
|
||||
|
||||
# Disk kullanımını al
|
||||
command = f"du -sh '{full_path}' 2>/dev/null | cut -f1"
|
||||
stdout, stderr, success = ssh_manager.execute_command(command)
|
||||
|
||||
if success and stdout.strip():
|
||||
old_usage = project.disk_usage or "Bilinmiyor"
|
||||
project.disk_usage = stdout.strip()
|
||||
project.save() # Veritabanına kaydet
|
||||
result_messages.append(f"Disk kullanımı güncellendi: {old_usage} → {project.disk_usage}")
|
||||
ssh_manager.close()
|
||||
return True, "; ".join(result_messages)
|
||||
else:
|
||||
result_messages.append("SSH bilgisi eksik")
|
||||
except Exception as e:
|
||||
result_messages.append(f"Disk kontrolü hatası: {str(e)}")
|
||||
ssh_manager.close()
|
||||
return False, "Disk kullanımı komutu başarısız"
|
||||
|
||||
# 2. Site durumunu kontrol et
|
||||
except Exception as e:
|
||||
if 'ssh_manager' in locals() and ssh_manager:
|
||||
ssh_manager.close()
|
||||
return False, f"Disk kontrolü hatası: {str(e)}"
|
||||
|
||||
def check_site_status(project):
|
||||
"""
|
||||
Projenin web sitesinin aktif olup olmadığını kontrol eder
|
||||
SADECE meta key doğrulaması ile site aktifliği belirlenir
|
||||
"""
|
||||
result_messages = []
|
||||
|
||||
# Site durumunu kontrol et
|
||||
if not project.url:
|
||||
project.last_site_check = timezone.now()
|
||||
project.save()
|
||||
return False, "; ".join(result_messages + ["URL eksik"])
|
||||
return False, "URL eksik"
|
||||
|
||||
# Meta key kontrolü zorunlu
|
||||
if not project.meta_key:
|
||||
project.is_site_active = False
|
||||
project.last_site_check = timezone.now()
|
||||
project.save()
|
||||
return False, "Meta key tanımlanmamış - Site doğrulaması için meta key gerekli"
|
||||
|
||||
try:
|
||||
# URL'yi düzenle
|
||||
@ -87,7 +106,7 @@ def check_site_status(project):
|
||||
if not url.startswith(('http://', 'https://')):
|
||||
url = f'http://{url}'
|
||||
|
||||
# Site kontrolü
|
||||
# Site kontrolü - sadece erişilebilirlik için
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
}
|
||||
@ -95,40 +114,107 @@ def check_site_status(project):
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
# Site erişilebilir
|
||||
project.is_site_active = True
|
||||
project.last_site_check = timezone.now()
|
||||
project.save()
|
||||
result_messages.append(f"Site aktif (HTTP {response.status_code})")
|
||||
return True, "; ".join(result_messages)
|
||||
# Site erişilebilir, şimdi ZORUNLU meta key kontrolü yapalım
|
||||
try:
|
||||
# HTML içeriğini parse et
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
# Meta tag'i farklı formatlarda ara
|
||||
meta_tags = []
|
||||
meta_tags.append(soup.find('meta', attrs={'name': 'site-verification'}))
|
||||
meta_tags.append(soup.find('meta', attrs={'name': 'site-verify'}))
|
||||
meta_tags.append(soup.find('meta', attrs={'name': 'verification'}))
|
||||
|
||||
# HTML içeriğinde meta key'i doğrudan ara
|
||||
html_content = response.text
|
||||
meta_key_in_html = project.meta_key in html_content
|
||||
|
||||
# Debug bilgisi ekle
|
||||
result_messages.append(f"HTML içeriğinde meta key aranıyor: {project.meta_key}")
|
||||
result_messages.append(f"HTML içeriğinde meta key bulundu: {meta_key_in_html}")
|
||||
|
||||
# Bulunan meta tag'leri kontrol et
|
||||
found_meta_tag = None
|
||||
for i, tag in enumerate(meta_tags):
|
||||
if tag:
|
||||
result_messages.append(f"Meta tag bulundu: {tag}")
|
||||
if tag.get('content') and project.meta_key in tag.get('content'):
|
||||
found_meta_tag = tag
|
||||
result_messages.append(f"Meta key, meta tag içeriğinde doğrulandı")
|
||||
break
|
||||
|
||||
# Tüm meta tag'lerde içerik kontrolü yap
|
||||
if not found_meta_tag:
|
||||
all_meta_tags = soup.find_all('meta')
|
||||
for tag in all_meta_tags:
|
||||
content = tag.get('content')
|
||||
if content and project.meta_key in content:
|
||||
found_meta_tag = tag
|
||||
result_messages.append(f"Meta key, '{tag.get('name', 'isimsiz')}' meta tag'de bulundu")
|
||||
break
|
||||
|
||||
# HTML yorum kontrolü (<!-- site-verification: key --> formatı için)
|
||||
comment_pattern = f'<!-- site-verification: {project.meta_key} -->'
|
||||
comment_found = comment_pattern in html_content
|
||||
if comment_found:
|
||||
result_messages.append("Meta key HTML yorum olarak bulundu")
|
||||
|
||||
# Meta key doğrulaması - ZORUNLU
|
||||
if found_meta_tag or meta_key_in_html or comment_found:
|
||||
# Meta key doğrulandı - Site AKTIF
|
||||
project.is_site_active = True
|
||||
project.last_site_check = timezone.now()
|
||||
project.save()
|
||||
|
||||
if found_meta_tag:
|
||||
return True, f"✅ Site AKTIF - Meta key meta tag'de doğrulandı"
|
||||
elif comment_found:
|
||||
return True, f"✅ Site AKTIF - Meta key HTML yorumunda doğrulandı"
|
||||
else:
|
||||
return True, f"✅ Site AKTIF - Meta key HTML içeriğinde doğrulandı"
|
||||
else:
|
||||
# Meta key bulunamadı - Site PASİF
|
||||
project.is_site_active = False
|
||||
project.last_site_check = timezone.now()
|
||||
project.save()
|
||||
return False, f"❌ Site PASİF - Site erişilebilir (HTTP {response.status_code}) ancak meta key doğrulanamadı"
|
||||
|
||||
except Exception as e:
|
||||
# Meta key kontrolünde hata - Site PASİF
|
||||
project.is_site_active = False
|
||||
project.last_site_check = timezone.now()
|
||||
project.save()
|
||||
return False, f"❌ Site PASİF - Meta key kontrolü hatası: {str(e)}"
|
||||
else:
|
||||
# Site erişilemez
|
||||
# Site erişilemez - Site PASİF
|
||||
project.is_site_active = False
|
||||
project.last_site_check = timezone.now()
|
||||
project.save()
|
||||
result_messages.append(f"Site erişilemez (HTTP {response.status_code})")
|
||||
return False, "; ".join(result_messages)
|
||||
return False, f"❌ Site PASİF - Site erişilemez (HTTP {response.status_code})"
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
project.is_site_active = False
|
||||
project.last_site_check = timezone.now()
|
||||
project.save()
|
||||
result_messages.append("Site zaman aşımı")
|
||||
return False, "; ".join(result_messages)
|
||||
return False, "❌ Site PASİF - Zaman aşımı hatası"
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
project.is_site_active = False
|
||||
project.last_site_check = timezone.now()
|
||||
project.save()
|
||||
result_messages.append("Site bağlantı hatası")
|
||||
return False, "; ".join(result_messages)
|
||||
|
||||
# DNS çözümleme hatası kontrolü
|
||||
error_str = str(e)
|
||||
if "NameResolutionError" in error_str or "getaddrinfo failed" in error_str:
|
||||
return False, f"❌ Site PASİF - Domain '{project.url}' DNS kayıtlarında bulunamadı"
|
||||
else:
|
||||
return False, f"❌ Site PASİF - Bağlantı hatası: {str(e)}"
|
||||
|
||||
except Exception as e:
|
||||
project.is_site_active = False
|
||||
project.last_site_check = timezone.now()
|
||||
project.save()
|
||||
result_messages.append(f"Site hatası: {str(e)}")
|
||||
return False, "; ".join(result_messages)
|
||||
return False, f"❌ Site PASİF - Genel hata: {str(e)}"
|
||||
|
||||
def check_all_sites():
|
||||
"""Tüm projelerin site durumunu kontrol et"""
|
||||
@ -136,11 +222,16 @@ def check_all_sites():
|
||||
results = []
|
||||
|
||||
for project in projects:
|
||||
status, message = check_site_status(project)
|
||||
site_status, site_message = check_site_status(project)
|
||||
disk_status, disk_message = check_disk_usage(project)
|
||||
|
||||
results.append({
|
||||
'project': project,
|
||||
'status': status,
|
||||
'message': message
|
||||
'status': site_status, # Site durumunu ana durum olarak kullan
|
||||
'site_message': site_message,
|
||||
'disk_status': disk_status,
|
||||
'disk_message': disk_message,
|
||||
'combined_message': f"Site: {site_message} | Disk: {disk_message}"
|
||||
})
|
||||
|
||||
return results
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user