starco-dj-utils 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dj_utils/__init__.py +0 -0
- dj_utils/admin.py +3 -0
- dj_utils/apps.py +9 -0
- dj_utils/handlers.py +83 -0
- dj_utils/management/__init__.py +0 -0
- dj_utils/management/commands/__init__.py +0 -0
- dj_utils/management/commands/backup.py +156 -0
- dj_utils/management/commands/restore.py +167 -0
- dj_utils/migrations/__init__.py +0 -0
- dj_utils/models.py +3 -0
- dj_utils/signals.py +7 -0
- dj_utils/tasks.py +29 -0
- dj_utils/tasks_config.py +11 -0
- dj_utils/templates/dj_utils/database_management.html +261 -0
- dj_utils/tests.py +3 -0
- dj_utils/urls.py +11 -0
- dj_utils/views.py +174 -0
- starco_dj_utils-1.0.2.dist-info/METADATA +16 -0
- starco_dj_utils-1.0.2.dist-info/RECORD +21 -0
- starco_dj_utils-1.0.2.dist-info/WHEEL +5 -0
- starco_dj_utils-1.0.2.dist-info/top_level.txt +1 -0
dj_utils/__init__.py
ADDED
|
File without changes
|
dj_utils/admin.py
ADDED
dj_utils/apps.py
ADDED
dj_utils/handlers.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from asgiref.sync import async_to_sync, sync_to_async
|
|
2
|
+
import os
|
|
3
|
+
from django.dispatch import receiver
|
|
4
|
+
from .signals import notifire, send_sms, send_mail
|
|
5
|
+
import re
|
|
6
|
+
from telegram import Bot
|
|
7
|
+
import logging
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
load_dotenv()
|
|
10
|
+
|
|
11
|
+
def get_bot():
|
|
12
|
+
TOKEN = os.getenv('UTILS_BOT_TOKEN')
|
|
13
|
+
if not TOKEN:
|
|
14
|
+
logging.error("Telegram bot token not found in environment variables.")
|
|
15
|
+
raise ValueError("Telegram bot token is required.\nUTILS_BOT_TOKEN must be set in environment variables.")
|
|
16
|
+
return Bot(token=TOKEN)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def clean_html_for_telegram(text: str) -> str:
|
|
20
|
+
"""
|
|
21
|
+
حذف تگهای HTML نامجاز برای استفاده در Telegram با parse_mode='HTML'
|
|
22
|
+
و جایگزینی برخی از آنها مثل <p> با newline
|
|
23
|
+
"""
|
|
24
|
+
# تگهای مجاز HTML برای تلگرام
|
|
25
|
+
allowed_tags = ['b', 'strong', 'i', 'em', 'u', 's', 'strike', 'del',
|
|
26
|
+
'span', 'a', 'code', 'pre']
|
|
27
|
+
|
|
28
|
+
# جایگزینی <p> و </p> با newline
|
|
29
|
+
text = re.sub(r'</?p\s*>', '\n', text)
|
|
30
|
+
|
|
31
|
+
# حذف تگهای غیرمجاز
|
|
32
|
+
def remove_invalid_tags(match):
|
|
33
|
+
tag = match.group(1)
|
|
34
|
+
if tag.lower() not in allowed_tags:
|
|
35
|
+
return ''
|
|
36
|
+
return match.group(0)
|
|
37
|
+
|
|
38
|
+
# حذف تگهای باز
|
|
39
|
+
text = re.sub(r'<(/?\s*?)(\w+)([^>]*)>',
|
|
40
|
+
lambda m: remove_invalid_tags((m.group(2),)) if m.group(2).lower() not in allowed_tags else m.group(
|
|
41
|
+
0), text)
|
|
42
|
+
|
|
43
|
+
return text.strip()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@receiver(notifire)
|
|
47
|
+
def handle_notifire(sender, text, chat_id=None, label=True, **kwargs):
|
|
48
|
+
chat_id = chat_id if chat_id else os.getenv('UTILS_TELEGRAM_CHAT_ID')
|
|
49
|
+
if not chat_id:
|
|
50
|
+
logging.error("Telegram chat ID not found in environment variables.")
|
|
51
|
+
raise ValueError("Telegram chat ID is required.\nUTILS_TELEGRAM_CHAT_ID must be set in environment variables.")
|
|
52
|
+
file = kwargs.get('file')
|
|
53
|
+
parse_mode = kwargs.get('parse_mode', 'HTML')
|
|
54
|
+
disable_notification = kwargs.get('disable_notification', False)
|
|
55
|
+
protect_content = kwargs.get('protect_content')
|
|
56
|
+
reply_markup = kwargs.get('reply_markup')
|
|
57
|
+
reply_to_message_id = kwargs.get('reply_to_message_id')
|
|
58
|
+
disable_web_page_preview = kwargs.get('disable_web_page_preview')
|
|
59
|
+
data = {
|
|
60
|
+
'chat_id': chat_id,
|
|
61
|
+
'parse_mode': parse_mode,
|
|
62
|
+
'disable_notification': disable_notification,
|
|
63
|
+
'protect_content': protect_content,
|
|
64
|
+
'reply_markup': reply_markup,
|
|
65
|
+
'reply_to_message_id': reply_to_message_id,
|
|
66
|
+
|
|
67
|
+
}
|
|
68
|
+
if label:
|
|
69
|
+
text = f"#{os.getenv('PROJECT_NAME_EV')}:{sender}\n{text}"
|
|
70
|
+
if kwargs.get('parse_mode') == 'HTML':
|
|
71
|
+
text = clean_html_for_telegram(text)
|
|
72
|
+
try:
|
|
73
|
+
bot = get_bot()
|
|
74
|
+
if file:
|
|
75
|
+
res = async_to_sync(bot.send_document)(document=file, caption=text, **data)
|
|
76
|
+
print(res)
|
|
77
|
+
|
|
78
|
+
else:
|
|
79
|
+
data['disable_web_page_preview'] = disable_web_page_preview
|
|
80
|
+
res = async_to_sync(bot.send_message)(text=text, **data)
|
|
81
|
+
print(res)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
print(e)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import datetime
|
|
4
|
+
from django.core.management.base import BaseCommand, CommandError
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Command(BaseCommand):
|
|
9
|
+
help = 'Backup database to a file'
|
|
10
|
+
|
|
11
|
+
def add_arguments(self, parser):
|
|
12
|
+
parser.add_argument('--path', type=str, help='Path to save the backup file (optional)')
|
|
13
|
+
parser.add_argument('--format', type=str, choices=['sql', 'dump', 'custom'], default='sql',
|
|
14
|
+
help='Backup format (sql, dump, or custom for PostgreSQL)')
|
|
15
|
+
|
|
16
|
+
def handle(self, *args, **options):
|
|
17
|
+
db_settings = settings.DATABASES['default']
|
|
18
|
+
db_type = db_settings['ENGINE'].split('.')[-1]
|
|
19
|
+
|
|
20
|
+
# Generate default backup path if not provided
|
|
21
|
+
backup_path = options.get('path')
|
|
22
|
+
if not backup_path:
|
|
23
|
+
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
24
|
+
backup_dir = os.path.join(settings.BASE_DIR, 'backups')
|
|
25
|
+
os.makedirs(backup_dir, exist_ok=True)
|
|
26
|
+
|
|
27
|
+
if db_type == 'postgresql':
|
|
28
|
+
ext = '.sql' if options['format'] == 'sql' else '.dump'
|
|
29
|
+
backup_path = os.path.join(backup_dir, f'backup_pg_{timestamp}{ext}')
|
|
30
|
+
elif db_type == 'sqlite3':
|
|
31
|
+
backup_path = os.path.join(backup_dir, f'backup_sqlite_{timestamp}.sqlite3')
|
|
32
|
+
elif db_type == 'mysql':
|
|
33
|
+
backup_path = os.path.join(backup_dir, f'backup_mysql_{timestamp}.sql')
|
|
34
|
+
else:
|
|
35
|
+
raise CommandError(f'Unsupported database type: {db_type}')
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
if db_type == 'postgresql':
|
|
39
|
+
self._backup_postgresql(db_settings, backup_path, options['format'])
|
|
40
|
+
elif db_type == 'sqlite3':
|
|
41
|
+
self._backup_sqlite3(db_settings, backup_path)
|
|
42
|
+
elif db_type == 'mysql':
|
|
43
|
+
self._backup_mysql(db_settings, backup_path)
|
|
44
|
+
else:
|
|
45
|
+
raise CommandError(f'Unsupported database type: {db_type}')
|
|
46
|
+
|
|
47
|
+
self.stdout.write(self.style.SUCCESS(f'Database backup created successfully at: {backup_path}'))
|
|
48
|
+
except Exception as e:
|
|
49
|
+
raise CommandError(f'Failed to backup database: {str(e)}')
|
|
50
|
+
|
|
51
|
+
def _backup_postgresql(self, db_settings, backup_path, format_type):
|
|
52
|
+
"""Backup PostgreSQL database to a file"""
|
|
53
|
+
db_name = db_settings['NAME']
|
|
54
|
+
db_user = db_settings['USER']
|
|
55
|
+
db_password = db_settings['PASSWORD']
|
|
56
|
+
db_host = db_settings['HOST']
|
|
57
|
+
db_port = db_settings['PORT']
|
|
58
|
+
|
|
59
|
+
env = os.environ.copy()
|
|
60
|
+
env['PGPASSWORD'] = db_password
|
|
61
|
+
|
|
62
|
+
if format_type == 'sql':
|
|
63
|
+
# Plain SQL dump
|
|
64
|
+
command = [
|
|
65
|
+
'pg_dump',
|
|
66
|
+
f'-h{db_host}',
|
|
67
|
+
f'-p{db_port}',
|
|
68
|
+
f'-U{db_user}',
|
|
69
|
+
'-d', db_name,
|
|
70
|
+
'-f', backup_path
|
|
71
|
+
]
|
|
72
|
+
else:
|
|
73
|
+
# Custom format dump
|
|
74
|
+
format_flag = '-Fc' if format_type == 'dump' else '-Fc' # Default to custom format
|
|
75
|
+
command = [
|
|
76
|
+
'pg_dump',
|
|
77
|
+
f'-h{db_host}',
|
|
78
|
+
f'-p{db_port}',
|
|
79
|
+
f'-U{db_user}',
|
|
80
|
+
format_flag,
|
|
81
|
+
'-d', db_name,
|
|
82
|
+
'-f', backup_path
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
process = subprocess.Popen(
|
|
86
|
+
command,
|
|
87
|
+
stdout=subprocess.PIPE,
|
|
88
|
+
stderr=subprocess.PIPE,
|
|
89
|
+
env=env
|
|
90
|
+
)
|
|
91
|
+
stdout, stderr = process.communicate()
|
|
92
|
+
|
|
93
|
+
if process.returncode != 0:
|
|
94
|
+
raise CommandError(f'pg_dump command failed: {stderr.decode()}')
|
|
95
|
+
|
|
96
|
+
def _backup_sqlite3(self, db_settings, backup_path):
|
|
97
|
+
"""Backup SQLite database to a file"""
|
|
98
|
+
db_path = db_settings['NAME']
|
|
99
|
+
|
|
100
|
+
if not os.path.exists(db_path):
|
|
101
|
+
raise CommandError(f'SQLite database file does not exist: {db_path}')
|
|
102
|
+
|
|
103
|
+
# For SQLite, we can simply copy the database file
|
|
104
|
+
import shutil
|
|
105
|
+
shutil.copy2(db_path, backup_path)
|
|
106
|
+
|
|
107
|
+
# Alternatively, we can use the .dump command for a SQL dump
|
|
108
|
+
if backup_path.endswith('.sql'):
|
|
109
|
+
command = [
|
|
110
|
+
'sqlite3',
|
|
111
|
+
db_path,
|
|
112
|
+
'.dump'
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
with open(backup_path, 'w') as f:
|
|
116
|
+
process = subprocess.Popen(
|
|
117
|
+
command,
|
|
118
|
+
stdout=f,
|
|
119
|
+
stderr=subprocess.PIPE
|
|
120
|
+
)
|
|
121
|
+
_, stderr = process.communicate()
|
|
122
|
+
|
|
123
|
+
if process.returncode != 0:
|
|
124
|
+
raise CommandError(f'sqlite3 dump command failed: {stderr.decode()}')
|
|
125
|
+
|
|
126
|
+
def _backup_mysql(self, db_settings, backup_path):
|
|
127
|
+
"""Backup MySQL database to a file"""
|
|
128
|
+
db_name = db_settings['NAME']
|
|
129
|
+
db_user = db_settings['USER']
|
|
130
|
+
db_password = db_settings['PASSWORD']
|
|
131
|
+
db_host = db_settings['HOST']
|
|
132
|
+
db_port = db_settings['PORT']
|
|
133
|
+
|
|
134
|
+
command = [
|
|
135
|
+
'mysqldump',
|
|
136
|
+
f'-h{db_host}',
|
|
137
|
+
f'-P{db_port}',
|
|
138
|
+
f'-u{db_user}',
|
|
139
|
+
f'-p{db_password}',
|
|
140
|
+
'--single-transaction',
|
|
141
|
+
'--routines',
|
|
142
|
+
'--triggers',
|
|
143
|
+
'--events',
|
|
144
|
+
db_name
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
with open(backup_path, 'w') as f:
|
|
148
|
+
process = subprocess.Popen(
|
|
149
|
+
command,
|
|
150
|
+
stdout=f,
|
|
151
|
+
stderr=subprocess.PIPE
|
|
152
|
+
)
|
|
153
|
+
_, stderr = process.communicate()
|
|
154
|
+
|
|
155
|
+
if process.returncode != 0:
|
|
156
|
+
raise CommandError(f'mysqldump command failed: {stderr.decode()}')
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from django.core.management.base import BaseCommand, CommandError
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Command(BaseCommand):
|
|
8
|
+
help = 'Restore database from a backup file'
|
|
9
|
+
|
|
10
|
+
def add_arguments(self, parser):
|
|
11
|
+
parser.add_argument('--path', type=str, required=True, help='Path to the backup file')
|
|
12
|
+
|
|
13
|
+
def handle(self, *args, **options):
|
|
14
|
+
backup_path = options['path']
|
|
15
|
+
|
|
16
|
+
if not os.path.exists(backup_path):
|
|
17
|
+
raise CommandError(f'Backup file does not exist: {backup_path}')
|
|
18
|
+
|
|
19
|
+
db_settings = settings.DATABASES['default']
|
|
20
|
+
db_type = db_settings['ENGINE'].split('.')[-1]
|
|
21
|
+
|
|
22
|
+
self.stdout.write(self.style.WARNING(f'Restoring database from {backup_path}'))
|
|
23
|
+
self.stdout.write(self.style.WARNING('This will overwrite the current database. Are you sure? (y/n)'))
|
|
24
|
+
|
|
25
|
+
# confirm = input()
|
|
26
|
+
# if confirm.lower() != 'y':
|
|
27
|
+
# self.stdout.write(self.style.ERROR('Restore cancelled.'))
|
|
28
|
+
# return
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
if db_type == 'postgresql':
|
|
32
|
+
self._restore_postgresql(db_settings, backup_path)
|
|
33
|
+
elif db_type == 'sqlite3':
|
|
34
|
+
self._restore_sqlite3(db_settings, backup_path)
|
|
35
|
+
elif db_type == 'mysql':
|
|
36
|
+
self._restore_mysql(db_settings, backup_path)
|
|
37
|
+
else:
|
|
38
|
+
raise CommandError(f'Unsupported database type: {db_type}')
|
|
39
|
+
|
|
40
|
+
self.stdout.write(self.style.SUCCESS('Database restored successfully!'))
|
|
41
|
+
except Exception as e:
|
|
42
|
+
raise CommandError(f'Failed to restore database: {str(e)}')
|
|
43
|
+
|
|
44
|
+
def _restore_postgresql(self, db_settings, backup_path):
|
|
45
|
+
"""Restore PostgreSQL database from backup file"""
|
|
46
|
+
db_name = db_settings['NAME']
|
|
47
|
+
db_user = db_settings['USER']
|
|
48
|
+
db_password = db_settings['PASSWORD']
|
|
49
|
+
db_host = db_settings['HOST']
|
|
50
|
+
db_port = db_settings['PORT']
|
|
51
|
+
|
|
52
|
+
# Check file extension to determine restore method
|
|
53
|
+
if backup_path.endswith('.sql'):
|
|
54
|
+
# SQL dump file
|
|
55
|
+
env = os.environ.copy()
|
|
56
|
+
env['PGPASSWORD'] = db_password
|
|
57
|
+
|
|
58
|
+
command = [
|
|
59
|
+
'psql',
|
|
60
|
+
f'-h{db_host}',
|
|
61
|
+
f'-p{db_port}',
|
|
62
|
+
f'-U{db_user}',
|
|
63
|
+
f'-d{db_name}',
|
|
64
|
+
'-f', backup_path
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
process = subprocess.Popen(
|
|
68
|
+
command,
|
|
69
|
+
stdout=subprocess.PIPE,
|
|
70
|
+
stderr=subprocess.PIPE,
|
|
71
|
+
env=env
|
|
72
|
+
)
|
|
73
|
+
stdout, stderr = process.communicate()
|
|
74
|
+
|
|
75
|
+
if process.returncode != 0:
|
|
76
|
+
raise CommandError(f'psql command failed: {stderr.decode()}')
|
|
77
|
+
|
|
78
|
+
elif backup_path.endswith('.dump'):
|
|
79
|
+
# pg_dump custom format
|
|
80
|
+
env = os.environ.copy()
|
|
81
|
+
env['PGPASSWORD'] = db_password
|
|
82
|
+
|
|
83
|
+
command = [
|
|
84
|
+
'pg_restore',
|
|
85
|
+
'--clean',
|
|
86
|
+
'--if-exists',
|
|
87
|
+
f'-h{db_host}',
|
|
88
|
+
f'-p{db_port}',
|
|
89
|
+
f'-U{db_user}',
|
|
90
|
+
f'-d{db_name}',
|
|
91
|
+
backup_path
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
process = subprocess.Popen(
|
|
95
|
+
command,
|
|
96
|
+
stdout=subprocess.PIPE,
|
|
97
|
+
stderr=subprocess.PIPE,
|
|
98
|
+
env=env
|
|
99
|
+
)
|
|
100
|
+
stdout, stderr = process.communicate()
|
|
101
|
+
|
|
102
|
+
if process.returncode != 0:
|
|
103
|
+
raise CommandError(f'pg_restore command failed: {stderr.decode()}')
|
|
104
|
+
else:
|
|
105
|
+
raise CommandError(f'Unsupported file format for PostgreSQL: {backup_path}')
|
|
106
|
+
|
|
107
|
+
def _restore_sqlite3(self, db_settings, backup_path):
|
|
108
|
+
"""Restore SQLite database from backup file"""
|
|
109
|
+
db_path = db_settings['NAME']
|
|
110
|
+
|
|
111
|
+
if os.path.exists(db_path):
|
|
112
|
+
# Create a backup of the current database
|
|
113
|
+
backup_name = f"{db_path}.bak"
|
|
114
|
+
os.rename(db_path, backup_name)
|
|
115
|
+
self.stdout.write(self.style.WARNING(f'Current database backed up to {backup_name}'))
|
|
116
|
+
|
|
117
|
+
# For SQLite, we can simply copy the backup file to the database location
|
|
118
|
+
if backup_path.endswith('.sql'):
|
|
119
|
+
# SQL dump file - need to execute the SQL commands
|
|
120
|
+
try:
|
|
121
|
+
import sqlite3
|
|
122
|
+
conn = sqlite3.connect(db_path)
|
|
123
|
+
cursor = conn.cursor()
|
|
124
|
+
|
|
125
|
+
with open(backup_path, 'r') as f:
|
|
126
|
+
sql_script = f.read()
|
|
127
|
+
cursor.executescript(sql_script)
|
|
128
|
+
|
|
129
|
+
conn.commit()
|
|
130
|
+
conn.close()
|
|
131
|
+
except Exception as e:
|
|
132
|
+
raise CommandError(f'Failed to restore SQLite database: {str(e)}')
|
|
133
|
+
else:
|
|
134
|
+
# Binary backup - just copy the file
|
|
135
|
+
import shutil
|
|
136
|
+
shutil.copy2(backup_path, db_path)
|
|
137
|
+
|
|
138
|
+
def _restore_mysql(self, db_settings, backup_path):
|
|
139
|
+
"""Restore MySQL database from backup file"""
|
|
140
|
+
db_name = db_settings['NAME']
|
|
141
|
+
db_user = db_settings['USER']
|
|
142
|
+
db_password = db_settings['PASSWORD']
|
|
143
|
+
db_host = db_settings['HOST']
|
|
144
|
+
db_port = db_settings['PORT']
|
|
145
|
+
|
|
146
|
+
command = [
|
|
147
|
+
'mysql',
|
|
148
|
+
f'-h{db_host}',
|
|
149
|
+
f'-P{db_port}',
|
|
150
|
+
f'-u{db_user}',
|
|
151
|
+
f'-p{db_password}',
|
|
152
|
+
db_name,
|
|
153
|
+
'<', backup_path
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
# For MySQL, we need to use shell=True to handle the redirect operator
|
|
157
|
+
command_str = ' '.join(command)
|
|
158
|
+
process = subprocess.Popen(
|
|
159
|
+
command_str,
|
|
160
|
+
stdout=subprocess.PIPE,
|
|
161
|
+
stderr=subprocess.PIPE,
|
|
162
|
+
shell=True
|
|
163
|
+
)
|
|
164
|
+
stdout, stderr = process.communicate()
|
|
165
|
+
|
|
166
|
+
if process.returncode != 0:
|
|
167
|
+
raise CommandError(f'mysql command failed: {stderr.decode()}')
|
|
File without changes
|
dj_utils/models.py
ADDED
dj_utils/signals.py
ADDED
dj_utils/tasks.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from celery import shared_task
|
|
2
|
+
import zipfile
|
|
3
|
+
import os
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from django.core.management import call_command
|
|
6
|
+
from .handlers import notifire
|
|
7
|
+
@shared_task
|
|
8
|
+
def backup_database():
|
|
9
|
+
path= '/app/backup.sql'
|
|
10
|
+
call_command('backup',path= path)
|
|
11
|
+
# Create ZIP archive
|
|
12
|
+
zip_file = f"{path}.zip"
|
|
13
|
+
with zipfile.ZipFile(zip_file, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
14
|
+
zipf.write(path, os.path.basename(path))
|
|
15
|
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
16
|
+
try:
|
|
17
|
+
with open(zip_file, 'rb') as backup:
|
|
18
|
+
notifire.send(
|
|
19
|
+
'backup_database',
|
|
20
|
+
chat_id=os.getenv('BACKUP_TELEGRAM_CHAT_ID_EV'), # Send to admin group
|
|
21
|
+
file=backup,
|
|
22
|
+
text=f"📂 Database Backup \n📅 {timestamp}\n💾 {os.path.getsize(zip_file) / (1024 * 1024):.2f} MB"
|
|
23
|
+
)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
print(e)
|
|
26
|
+
notifire.send(sender='backup_database', text=f"❌ Error sending backup: {str(e)}")
|
|
27
|
+
os.remove(zip_file)
|
|
28
|
+
os.remove(path)
|
|
29
|
+
|
dj_utils/tasks_config.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
{% load static %}
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html lang="fa" dir="rtl">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>{{ title }}</title>
|
|
8
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
9
|
+
<style>
|
|
10
|
+
@keyframes slideIn {
|
|
11
|
+
from {
|
|
12
|
+
opacity: 0;
|
|
13
|
+
transform: translateY(-20px);
|
|
14
|
+
}
|
|
15
|
+
to {
|
|
16
|
+
opacity: 1;
|
|
17
|
+
transform: translateY(0);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@keyframes spin {
|
|
22
|
+
0% { transform: rotate(0deg); }
|
|
23
|
+
100% { transform: rotate(360deg); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.animate-slide-in {
|
|
27
|
+
animation: slideIn 0.3s ease;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.spinner {
|
|
31
|
+
border: 4px solid #f3f3f3;
|
|
32
|
+
border-top: 4px solid #667eea;
|
|
33
|
+
border-radius: 50%;
|
|
34
|
+
width: 40px;
|
|
35
|
+
height: 40px;
|
|
36
|
+
animation: spin 1s linear infinite;
|
|
37
|
+
margin: 0 auto;
|
|
38
|
+
}
|
|
39
|
+
</style>
|
|
40
|
+
</head>
|
|
41
|
+
<body class="bg-gradient-to-br from-purple-600 to-purple-800 min-h-screen p-4 md:p-8">
|
|
42
|
+
<div class="max-w-4xl mx-auto">
|
|
43
|
+
<!-- Header -->
|
|
44
|
+
<div class="bg-white rounded-t-2xl shadow-2xl overflow-hidden">
|
|
45
|
+
<div class="bg-gradient-to-r from-purple-600 to-purple-800 text-white p-8">
|
|
46
|
+
<h1 class="text-3xl md:text-4xl font-bold text-center mb-4">
|
|
47
|
+
🗄️ {{ title }}
|
|
48
|
+
</h1>
|
|
49
|
+
<div class="bg-white/10 backdrop-blur-sm rounded-xl p-4 space-y-2">
|
|
50
|
+
<p class="text-sm md:text-base">
|
|
51
|
+
<span class="font-semibold">موتور دیتابیس:</span>
|
|
52
|
+
<span class="font-mono">{{ db_engine }}</span>
|
|
53
|
+
</p>
|
|
54
|
+
<p class="text-sm md:text-base">
|
|
55
|
+
<span class="font-semibold">نام دیتابیس:</span>
|
|
56
|
+
<span class="font-mono">{{ db_name }}</span>
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<!-- Content -->
|
|
63
|
+
<div class="bg-white rounded-b-2xl shadow-2xl p-6 md:p-10">
|
|
64
|
+
<!-- Messages -->
|
|
65
|
+
{% if messages %}
|
|
66
|
+
<div class="mb-6 space-y-3">
|
|
67
|
+
{% for message in messages %}
|
|
68
|
+
<div class="animate-slide-in rounded-lg p-4 {% if message.tags == 'success' %}bg-green-50 text-green-800 border border-green-200{% elif message.tags == 'error' %}bg-red-50 text-red-800 border border-red-200{% else %}bg-blue-50 text-blue-800 border border-blue-200{% endif %}">
|
|
69
|
+
<p class="text-sm md:text-base">{{ message }}</p>
|
|
70
|
+
</div>
|
|
71
|
+
{% endfor %}
|
|
72
|
+
</div>
|
|
73
|
+
{% endif %}
|
|
74
|
+
|
|
75
|
+
<!-- Backup Section -->
|
|
76
|
+
<div class="bg-gradient-to-br from-green-50 to-emerald-50 rounded-xl p-6 md:p-8 mb-6 border border-green-100">
|
|
77
|
+
<h2 class="text-2xl font-bold text-green-700 mb-4 flex items-center gap-2">
|
|
78
|
+
<span>📦</span>
|
|
79
|
+
<span>ایجاد بکآپ</span>
|
|
80
|
+
</h2>
|
|
81
|
+
<p class="text-gray-700 mb-6 leading-relaxed">
|
|
82
|
+
با استفاده از این بخش میتوانید از دیتابیس خود بکآپ تهیه کنید. فایل بکآپ به صورت فشرده (ZIP) دانلود خواهد شد.
|
|
83
|
+
</p>
|
|
84
|
+
|
|
85
|
+
<form method="post" action="{% url 'database_backup' %}" id="backupForm">
|
|
86
|
+
{% csrf_token %}
|
|
87
|
+
<button type="submit" class="bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white font-bold py-3 px-8 rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 flex items-center gap-2">
|
|
88
|
+
<span>💾</span>
|
|
89
|
+
<span>دانلود بکآپ</span>
|
|
90
|
+
</button>
|
|
91
|
+
</form>
|
|
92
|
+
|
|
93
|
+
<div class="hidden mt-6 text-center" id="backupLoading">
|
|
94
|
+
<div class="spinner mb-3"></div>
|
|
95
|
+
<p class="text-gray-600 font-medium">در حال ایجاد بکآپ...</p>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<!-- Restore Section -->
|
|
100
|
+
<div class="bg-gradient-to-br from-purple-50 to-indigo-50 rounded-xl p-6 md:p-8 border border-purple-100">
|
|
101
|
+
<h2 class="text-2xl font-bold text-purple-700 mb-4 flex items-center gap-2">
|
|
102
|
+
<span>♻️</span>
|
|
103
|
+
<span>بازیابی دیتابیس</span>
|
|
104
|
+
</h2>
|
|
105
|
+
<p class="text-gray-700 mb-4 leading-relaxed">
|
|
106
|
+
برای بازیابی دیتابیس، فایل بکآپ (.sql، .dump یا .zip) خود را انتخاب کنید.
|
|
107
|
+
</p>
|
|
108
|
+
|
|
109
|
+
<!-- Warning -->
|
|
110
|
+
<div class="bg-yellow-50 border-r-4 border-yellow-400 p-4 mb-6 rounded-lg">
|
|
111
|
+
<div class="flex items-start gap-3">
|
|
112
|
+
<span class="text-2xl">⚠️</span>
|
|
113
|
+
<div>
|
|
114
|
+
<p class="font-bold text-yellow-800 mb-2">هشدار مهم:</p>
|
|
115
|
+
<p class="text-yellow-700 text-sm leading-relaxed">
|
|
116
|
+
بازیابی دیتابیس تمام اطلاعات فعلی را جایگزین میکند. قبل از ادامه، مطمئن شوید که از دیتابیس فعلی بکآپ تهیه کردهاید.
|
|
117
|
+
</p>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<form method="post" action="{% url 'database_restore' %}" enctype="multipart/form-data" id="restoreForm">
|
|
123
|
+
{% csrf_token %}
|
|
124
|
+
|
|
125
|
+
<div class="mb-6">
|
|
126
|
+
<label class="block text-gray-700 font-semibold mb-3">
|
|
127
|
+
انتخاب فایل بکآپ:
|
|
128
|
+
</label>
|
|
129
|
+
|
|
130
|
+
<div class="flex flex-col md:flex-row items-start md:items-center gap-4">
|
|
131
|
+
<label class="cursor-pointer bg-white hover:bg-gray-50 text-purple-600 font-semibold py-3 px-6 rounded-lg border-2 border-purple-300 hover:border-purple-400 transition-all duration-200 flex items-center gap-2">
|
|
132
|
+
<span>📁</span>
|
|
133
|
+
<span>انتخاب فایل</span>
|
|
134
|
+
<input type="file" name="backup_file" id="backupFile" accept=".sql,.dump,.zip" class="hidden" required>
|
|
135
|
+
</label>
|
|
136
|
+
|
|
137
|
+
<div class="flex-1 w-full md:w-auto">
|
|
138
|
+
<div id="fileName" class="bg-white border-2 border-dashed border-purple-300 rounded-lg p-3 text-gray-500 text-sm min-w-[200px]">
|
|
139
|
+
هیچ فایلی انتخاب نشده
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<p class="text-xs text-gray-500 mt-2">
|
|
145
|
+
فرمتهای مجاز: .sql، .dump، .zip
|
|
146
|
+
</p>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div class="flex flex-col sm:flex-row gap-3">
|
|
150
|
+
<button type="submit" id="restoreBtn" class="bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-bold py-3 px-8 rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none flex items-center justify-center gap-2" disabled>
|
|
151
|
+
<span>♻️</span>
|
|
152
|
+
<span>بازیابی دیتابیس</span>
|
|
153
|
+
</button>
|
|
154
|
+
|
|
155
|
+
<button type="button" onclick="window.location.href='{% url 'database_management' %}'" class="bg-gray-200 hover:bg-gray-300 text-gray-700 font-semibold py-3 px-8 rounded-lg transition-all duration-200 flex items-center justify-center gap-2">
|
|
156
|
+
<span>↩️</span>
|
|
157
|
+
<span>بازگشت</span>
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
</form>
|
|
161
|
+
|
|
162
|
+
<div class="hidden mt-6 text-center" id="restoreLoading">
|
|
163
|
+
<div class="spinner mb-3"></div>
|
|
164
|
+
<p class="text-gray-600 font-medium">در حال بازیابی دیتابیس...</p>
|
|
165
|
+
<p class="text-sm text-gray-500 mt-2">این عملیات ممکن است چند دقیقه طول بکشد...</p>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<!-- Info Section -->
|
|
170
|
+
<div class="mt-6 bg-blue-50 border border-blue-200 rounded-xl p-6">
|
|
171
|
+
<h3 class="text-lg font-bold text-blue-800 mb-3 flex items-center gap-2">
|
|
172
|
+
<span>ℹ️</span>
|
|
173
|
+
<span>راهنما</span>
|
|
174
|
+
</h3>
|
|
175
|
+
<ul class="space-y-2 text-sm text-blue-700">
|
|
176
|
+
<li class="flex items-start gap-2">
|
|
177
|
+
<span class="mt-1">•</span>
|
|
178
|
+
<span>برای ایجاد بکآپ، روی دکمه "دانلود بکآپ" کلیک کنید</span>
|
|
179
|
+
</li>
|
|
180
|
+
<li class="flex items-start gap-2">
|
|
181
|
+
<span class="mt-1">•</span>
|
|
182
|
+
<span>فایل بکآپ به صورت خودکار فشرده و دانلود میشود</span>
|
|
183
|
+
</li>
|
|
184
|
+
<li class="flex items-start gap-2">
|
|
185
|
+
<span class="mt-1">•</span>
|
|
186
|
+
<span>برای بازیابی، فایل بکآپ را انتخاب کرده و روی "بازیابی دیتابیس" کلیک کنید</span>
|
|
187
|
+
</li>
|
|
188
|
+
<li class="flex items-start gap-2">
|
|
189
|
+
<span class="mt-1">•</span>
|
|
190
|
+
<span>حتماً قبل از بازیابی، از دیتابیس فعلی بکآپ تهیه کنید</span>
|
|
191
|
+
</li>
|
|
192
|
+
</ul>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<script>
|
|
198
|
+
// File input handler
|
|
199
|
+
const fileInput = document.getElementById('backupFile');
|
|
200
|
+
const fileName = document.getElementById('fileName');
|
|
201
|
+
const restoreBtn = document.getElementById('restoreBtn');
|
|
202
|
+
|
|
203
|
+
fileInput.addEventListener('change', function(e) {
|
|
204
|
+
if (this.files && this.files[0]) {
|
|
205
|
+
const file = this.files[0];
|
|
206
|
+
const fileSize = (file.size / (1024 * 1024)).toFixed(2);
|
|
207
|
+
fileName.innerHTML = `
|
|
208
|
+
<div class="flex items-center justify-between">
|
|
209
|
+
<span class="text-gray-700 font-medium truncate">${file.name}</span>
|
|
210
|
+
<span class="text-gray-500 text-xs mr-2">${fileSize} MB</span>
|
|
211
|
+
</div>
|
|
212
|
+
`;
|
|
213
|
+
fileName.classList.remove('border-dashed', 'text-gray-500');
|
|
214
|
+
fileName.classList.add('border-solid', 'border-green-400', 'bg-green-50');
|
|
215
|
+
restoreBtn.disabled = false;
|
|
216
|
+
} else {
|
|
217
|
+
fileName.textContent = 'هیچ فایلی انتخاب نشده';
|
|
218
|
+
fileName.classList.add('border-dashed', 'text-gray-500');
|
|
219
|
+
fileName.classList.remove('border-solid', 'border-green-400', 'bg-green-50');
|
|
220
|
+
restoreBtn.disabled = true;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Backup form handler
|
|
225
|
+
const backupForm = document.getElementById('backupForm');
|
|
226
|
+
const backupLoading = document.getElementById('backupLoading');
|
|
227
|
+
|
|
228
|
+
backupForm.addEventListener('submit', function() {
|
|
229
|
+
backupLoading.classList.remove('hidden');
|
|
230
|
+
this.querySelector('button').disabled = true;
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Restore form handler
|
|
234
|
+
const restoreForm = document.getElementById('restoreForm');
|
|
235
|
+
const restoreLoading = document.getElementById('restoreLoading');
|
|
236
|
+
|
|
237
|
+
restoreForm.addEventListener('submit', function(e) {
|
|
238
|
+
if (!confirm('⚠️ آیا مطمئن هستید که میخواهید دیتابیس را بازیابی کنید؟\n\nتمام اطلاعات فعلی جایگزین خواهند شد!')) {
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
restoreLoading.classList.remove('hidden');
|
|
244
|
+
restoreBtn.disabled = true;
|
|
245
|
+
restoreBtn.innerHTML = '<span class="spinner-small"></span> در حال بازیابی...';
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Auto-hide messages after 5 seconds
|
|
249
|
+
setTimeout(function() {
|
|
250
|
+
const messages = document.querySelectorAll('.animate-slide-in');
|
|
251
|
+
messages.forEach(function(msg) {
|
|
252
|
+
msg.style.transition = 'opacity 0.5s ease';
|
|
253
|
+
msg.style.opacity = '0';
|
|
254
|
+
setTimeout(function() {
|
|
255
|
+
msg.remove();
|
|
256
|
+
}, 500);
|
|
257
|
+
});
|
|
258
|
+
}, 5000);
|
|
259
|
+
</script>
|
|
260
|
+
</body>
|
|
261
|
+
</html>
|
dj_utils/tests.py
ADDED
dj_utils/urls.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
from django.urls import path
|
|
3
|
+
from . import views
|
|
4
|
+
|
|
5
|
+
urlpatterns = [
|
|
6
|
+
# Database Management URLs
|
|
7
|
+
path('database/', views.database_management_view, name='database_management'),
|
|
8
|
+
path('database/backup/', views.database_backup_view, name='database_backup'),
|
|
9
|
+
path('database/restore/', views.database_restore_view, name='database_restore'),
|
|
10
|
+
path('database/backup/ajax/', views.database_backup_ajax, name='database_backup_ajax'),
|
|
11
|
+
]
|
dj_utils/views.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
|
|
2
|
+
from django.shortcuts import render, redirect
|
|
3
|
+
from django.contrib.admin.views.decorators import staff_member_required
|
|
4
|
+
from django.contrib.auth.decorators import user_passes_test
|
|
5
|
+
from django.http import JsonResponse, HttpResponse, FileResponse
|
|
6
|
+
from django.core.management import call_command
|
|
7
|
+
from django.views.decorators.http import require_http_methods
|
|
8
|
+
from django.contrib import messages
|
|
9
|
+
from django.conf import settings
|
|
10
|
+
import os
|
|
11
|
+
import zipfile
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
import tempfile
|
|
14
|
+
from io import BytesIO
|
|
15
|
+
|
|
16
|
+
def is_superuser(user):
|
|
17
|
+
"""Check if user is superuser"""
|
|
18
|
+
return user.is_superuser
|
|
19
|
+
|
|
20
|
+
@user_passes_test(is_superuser)
|
|
21
|
+
@require_http_methods(["GET", "POST"])
|
|
22
|
+
def database_backup_view(request):
|
|
23
|
+
"""
|
|
24
|
+
View for creating database backup
|
|
25
|
+
Only accessible by superusers
|
|
26
|
+
"""
|
|
27
|
+
if request.method == "POST":
|
|
28
|
+
try:
|
|
29
|
+
# Create temporary backup file
|
|
30
|
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
31
|
+
backup_filename = f'backup_{timestamp}.sql'
|
|
32
|
+
backup_path = os.path.join(tempfile.gettempdir(), backup_filename)
|
|
33
|
+
|
|
34
|
+
# Call backup command
|
|
35
|
+
call_command('backup', path=backup_path)
|
|
36
|
+
|
|
37
|
+
# Create ZIP archive
|
|
38
|
+
zip_buffer = BytesIO()
|
|
39
|
+
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
40
|
+
zipf.write(backup_path, os.path.basename(backup_path))
|
|
41
|
+
|
|
42
|
+
# Clean up temporary file
|
|
43
|
+
if os.path.exists(backup_path):
|
|
44
|
+
os.remove(backup_path)
|
|
45
|
+
|
|
46
|
+
# Prepare response
|
|
47
|
+
zip_buffer.seek(0)
|
|
48
|
+
response = HttpResponse(zip_buffer.getvalue(), content_type='application/zip')
|
|
49
|
+
response['Content-Disposition'] = f'attachment; filename="backup_{timestamp}.zip"'
|
|
50
|
+
|
|
51
|
+
messages.success(request, f'✅ بکآپ با موفقیت ایجاد شد: {backup_filename}')
|
|
52
|
+
return response
|
|
53
|
+
|
|
54
|
+
except Exception as e:
|
|
55
|
+
messages.error(request, f'❌ خطا در ایجاد بکآپ: {str(e)}')
|
|
56
|
+
return redirect('database_management')
|
|
57
|
+
|
|
58
|
+
return redirect('database_management')
|
|
59
|
+
|
|
60
|
+
@user_passes_test(is_superuser)
|
|
61
|
+
@require_http_methods(["GET", "POST"])
|
|
62
|
+
def database_restore_view(request):
|
|
63
|
+
"""
|
|
64
|
+
View for restoring database from backup
|
|
65
|
+
Only accessible by superusers
|
|
66
|
+
"""
|
|
67
|
+
if request.method == "POST":
|
|
68
|
+
backup_file = request.FILES.get('backup_file')
|
|
69
|
+
|
|
70
|
+
if not backup_file:
|
|
71
|
+
messages.error(request, '❌ لطفاً فایل بکآپ را انتخاب کنید')
|
|
72
|
+
return redirect('database_management')
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
# Save uploaded file temporarily
|
|
76
|
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
77
|
+
temp_dir = tempfile.gettempdir()
|
|
78
|
+
|
|
79
|
+
# Check if file is ZIP
|
|
80
|
+
if backup_file.name.endswith('.zip'):
|
|
81
|
+
zip_path = os.path.join(temp_dir, f'restore_{timestamp}.zip')
|
|
82
|
+
with open(zip_path, 'wb+') as destination:
|
|
83
|
+
for chunk in backup_file.chunks():
|
|
84
|
+
destination.write(chunk)
|
|
85
|
+
|
|
86
|
+
# Extract ZIP file
|
|
87
|
+
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
|
88
|
+
zip_ref.extractall(temp_dir)
|
|
89
|
+
extracted_files = zip_ref.namelist()
|
|
90
|
+
|
|
91
|
+
if not extracted_files:
|
|
92
|
+
raise Exception('فایل ZIP خالی است')
|
|
93
|
+
|
|
94
|
+
backup_path = os.path.join(temp_dir, extracted_files[0])
|
|
95
|
+
|
|
96
|
+
# Clean up ZIP file
|
|
97
|
+
os.remove(zip_path)
|
|
98
|
+
else:
|
|
99
|
+
# Direct SQL or dump file
|
|
100
|
+
backup_path = os.path.join(temp_dir, f'restore_{timestamp}{os.path.splitext(backup_file.name)[1]}')
|
|
101
|
+
with open(backup_path, 'wb+') as destination:
|
|
102
|
+
for chunk in backup_file.chunks():
|
|
103
|
+
destination.write(chunk)
|
|
104
|
+
|
|
105
|
+
# Validate file extension
|
|
106
|
+
if not (backup_path.endswith('.sql') or backup_path.endswith('.dump')):
|
|
107
|
+
os.remove(backup_path)
|
|
108
|
+
raise Exception('فرمت فایل پشتیبانی نشده است. فقط .sql یا .dump مجاز است')
|
|
109
|
+
|
|
110
|
+
# Call restore command
|
|
111
|
+
call_command('restore', path=backup_path)
|
|
112
|
+
|
|
113
|
+
# Clean up temporary file
|
|
114
|
+
if os.path.exists(backup_path):
|
|
115
|
+
os.remove(backup_path)
|
|
116
|
+
|
|
117
|
+
messages.success(request, '✅ دیتابیس با موفقیت بازیابی شد')
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
messages.error(request, f'❌ خطا در بازیابی دیتابیس: {str(e)}')
|
|
121
|
+
|
|
122
|
+
# Clean up on error
|
|
123
|
+
if 'backup_path' in locals() and os.path.exists(backup_path):
|
|
124
|
+
os.remove(backup_path)
|
|
125
|
+
|
|
126
|
+
return redirect('database_management')
|
|
127
|
+
|
|
128
|
+
@user_passes_test(is_superuser)
|
|
129
|
+
def database_management_view(request):
|
|
130
|
+
"""
|
|
131
|
+
Main view for database management
|
|
132
|
+
Shows backup and restore options
|
|
133
|
+
"""
|
|
134
|
+
context = {
|
|
135
|
+
'title': 'مدیریت دیتابیس',
|
|
136
|
+
'db_engine': settings.DATABASES['default']['ENGINE'],
|
|
137
|
+
'db_name': settings.DATABASES['default']['NAME'],
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return render(request, 'dj_utils/database_management.html', context)
|
|
141
|
+
|
|
142
|
+
@user_passes_test(is_superuser)
|
|
143
|
+
@require_http_methods(["POST"])
|
|
144
|
+
def database_backup_ajax(request):
|
|
145
|
+
"""
|
|
146
|
+
AJAX endpoint for database backup
|
|
147
|
+
Returns JSON response
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
151
|
+
backup_filename = f'backup_{timestamp}.sql'
|
|
152
|
+
backup_path = os.path.join(tempfile.gettempdir(), backup_filename)
|
|
153
|
+
|
|
154
|
+
call_command('backup', path=backup_path)
|
|
155
|
+
|
|
156
|
+
file_size = os.path.getsize(backup_path) / (1024 * 1024) # Size in MB
|
|
157
|
+
|
|
158
|
+
# Clean up
|
|
159
|
+
if os.path.exists(backup_path):
|
|
160
|
+
os.remove(backup_path)
|
|
161
|
+
|
|
162
|
+
return JsonResponse({
|
|
163
|
+
'success': True,
|
|
164
|
+
'message': f'بکآپ با موفقیت ایجاد شد',
|
|
165
|
+
'filename': backup_filename,
|
|
166
|
+
'size': f'{file_size:.2f} MB',
|
|
167
|
+
'timestamp': timestamp
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
return JsonResponse({
|
|
172
|
+
'success': False,
|
|
173
|
+
'message': f'خطا در ایجاد بکآپ: {str(e)}'
|
|
174
|
+
}, status=500)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: starco-dj-utils
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: A Django pluggable app for utilities and database management
|
|
5
|
+
Author: Mojtaba
|
|
6
|
+
Author-email: m.tahmasbi0111@yahoo.com
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Framework :: Django
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Requires-Dist: Django>=4.0
|
|
11
|
+
Dynamic: author
|
|
12
|
+
Dynamic: author-email
|
|
13
|
+
Dynamic: classifier
|
|
14
|
+
Dynamic: license
|
|
15
|
+
Dynamic: requires-dist
|
|
16
|
+
Dynamic: summary
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
dj_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
dj_utils/admin.py,sha256=suMo4x8I3JBxAFBVIdE-5qnqZ6JAZV0FESABHOSc-vg,63
|
|
3
|
+
dj_utils/apps.py,sha256=4Hj6VLtqAhiItX4Ifztaj5DnP7csfHKk_kCtTmPAC8w,234
|
|
4
|
+
dj_utils/handlers.py,sha256=4BAmPAV1gByCrZHpUyXc9qxpqIJeT2cLCV8p_F-cJCc,3102
|
|
5
|
+
dj_utils/models.py,sha256=Vjc0p2XbAPgE6HyTF6vll98A4eDhA5AvaQqsc4kQ9AQ,57
|
|
6
|
+
dj_utils/signals.py,sha256=1UsFrBjhrJPL7mExvD6SExgYAl1oqU8SDdZVmr65NF8,99
|
|
7
|
+
dj_utils/tasks.py,sha256=vBCfY_cE_tbS6I-0NyUm4fwf5_QfvUvyAjm0GYoJSiY,1047
|
|
8
|
+
dj_utils/tasks_config.py,sha256=aRjQ4xcbFWRY3XwCyAeBAqxS1ZfIPYBZBXNs4LnONGo,276
|
|
9
|
+
dj_utils/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2nk6VY-g,60
|
|
10
|
+
dj_utils/urls.py,sha256=sKZ0c7IxR6Z-reGRhDilLq4ZOldD4ZC_t5uN_SPIP3Y,442
|
|
11
|
+
dj_utils/views.py,sha256=6HQBbaDiYIpYoRlYrTs3pKNTfzj9U4xXaFxkg9PBXWc,6599
|
|
12
|
+
dj_utils/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
dj_utils/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
dj_utils/management/commands/backup.py,sha256=2LVTUyeKpyvrjEzliGbyehYNk-umXFasSDr1qHZkhj0,5573
|
|
15
|
+
dj_utils/management/commands/restore.py,sha256=hg_S-Rvd83pyyG9-nML99Fh6htZHkaQgmTkMUc4we3k,5809
|
|
16
|
+
dj_utils/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
dj_utils/templates/dj_utils/database_management.html,sha256=KMOseSxu_RGujuEwSpeXrp2kYm61FISRhKyk9L5Khbo,13248
|
|
18
|
+
starco_dj_utils-1.0.2.dist-info/METADATA,sha256=Qza2AyGxRVAbmlmUV7I7QPFMw5xxlO29kNR9otso_T8,419
|
|
19
|
+
starco_dj_utils-1.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
+
starco_dj_utils-1.0.2.dist-info/top_level.txt,sha256=rsoZCEk78JSKSSx0ac_lRbFiZBJpicuobSbw2l0cxvk,9
|
|
21
|
+
starco_dj_utils-1.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dj_utils
|