db-sync-tool-kmi 2.11.6__py3-none-any.whl → 3.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.
- db_sync_tool/__main__.py +7 -252
- db_sync_tool/cli.py +733 -0
- db_sync_tool/database/process.py +94 -111
- db_sync_tool/database/utility.py +339 -121
- db_sync_tool/info.py +1 -1
- db_sync_tool/recipes/drupal.py +87 -12
- db_sync_tool/recipes/laravel.py +7 -6
- db_sync_tool/recipes/parsing.py +102 -0
- db_sync_tool/recipes/symfony.py +17 -28
- db_sync_tool/recipes/typo3.py +33 -54
- db_sync_tool/recipes/wordpress.py +13 -12
- db_sync_tool/remote/client.py +206 -71
- db_sync_tool/remote/file_transfer.py +303 -0
- db_sync_tool/remote/rsync.py +18 -15
- db_sync_tool/remote/system.py +2 -3
- db_sync_tool/remote/transfer.py +51 -47
- db_sync_tool/remote/utility.py +29 -30
- db_sync_tool/sync.py +52 -28
- db_sync_tool/utility/config.py +367 -0
- db_sync_tool/utility/config_resolver.py +573 -0
- db_sync_tool/utility/console.py +779 -0
- db_sync_tool/utility/exceptions.py +32 -0
- db_sync_tool/utility/helper.py +155 -148
- db_sync_tool/utility/info.py +53 -20
- db_sync_tool/utility/log.py +55 -31
- db_sync_tool/utility/logging_config.py +410 -0
- db_sync_tool/utility/mode.py +85 -150
- db_sync_tool/utility/output.py +122 -51
- db_sync_tool/utility/parser.py +33 -53
- db_sync_tool/utility/pure.py +93 -0
- db_sync_tool/utility/security.py +79 -0
- db_sync_tool/utility/system.py +277 -194
- db_sync_tool/utility/validation.py +2 -9
- db_sync_tool_kmi-3.0.2.dist-info/METADATA +99 -0
- db_sync_tool_kmi-3.0.2.dist-info/RECORD +44 -0
- {db_sync_tool_kmi-2.11.6.dist-info → db_sync_tool_kmi-3.0.2.dist-info}/WHEEL +1 -1
- db_sync_tool_kmi-2.11.6.dist-info/METADATA +0 -276
- db_sync_tool_kmi-2.11.6.dist-info/RECORD +0 -34
- {db_sync_tool_kmi-2.11.6.dist-info → db_sync_tool_kmi-3.0.2.dist-info}/entry_points.txt +0 -0
- {db_sync_tool_kmi-2.11.6.dist-info → db_sync_tool_kmi-3.0.2.dist-info/licenses}/LICENSE +0 -0
- {db_sync_tool_kmi-2.11.6.dist-info → db_sync_tool_kmi-3.0.2.dist-info}/top_level.txt +0 -0
db_sync_tool/database/utility.py
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: future_fstrings -*-
|
|
3
2
|
|
|
4
3
|
"""
|
|
5
4
|
Utility script
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
|
-
import sys
|
|
9
7
|
import datetime
|
|
10
8
|
import re
|
|
9
|
+
import os
|
|
10
|
+
import secrets
|
|
11
|
+
import base64
|
|
11
12
|
from db_sync_tool.utility import mode, system, helper, output
|
|
13
|
+
from db_sync_tool.utility.security import sanitize_table_name # noqa: F401 (re-export)
|
|
14
|
+
from db_sync_tool.utility.exceptions import ConfigError, DbSyncError
|
|
12
15
|
|
|
13
|
-
database_dump_file_name = None
|
|
16
|
+
database_dump_file_name: str | None = None
|
|
17
|
+
|
|
18
|
+
# Track MySQL config files for cleanup (client -> path)
|
|
19
|
+
_mysql_config_files: dict[str, str] = {}
|
|
14
20
|
|
|
15
21
|
|
|
16
22
|
class DatabaseSystem:
|
|
@@ -18,80 +24,209 @@ class DatabaseSystem:
|
|
|
18
24
|
MARIADB = 'MariaDB'
|
|
19
25
|
|
|
20
26
|
|
|
21
|
-
def
|
|
27
|
+
def create_mysql_config_file(client: str) -> str:
|
|
22
28
|
"""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
:param
|
|
27
|
-
:return:
|
|
29
|
+
Create a secure temporary MySQL config file with credentials.
|
|
30
|
+
This prevents passwords from appearing in process lists (ps aux).
|
|
31
|
+
|
|
32
|
+
:param client: String client identifier ('origin' or 'target')
|
|
33
|
+
:return: String path to the config file
|
|
34
|
+
"""
|
|
35
|
+
global _mysql_config_files
|
|
36
|
+
|
|
37
|
+
cfg = system.get_typed_config()
|
|
38
|
+
client_cfg = cfg.get_client(client)
|
|
39
|
+
|
|
40
|
+
# Verify database config exists
|
|
41
|
+
if not client_cfg.db.name and not client_cfg.db.user:
|
|
42
|
+
raise ConfigError(f"Database configuration not found for client: {client}")
|
|
43
|
+
|
|
44
|
+
db_config = client_cfg.db
|
|
45
|
+
|
|
46
|
+
# Build config file content
|
|
47
|
+
# Passwords must be quoted to handle special chars like # ; $ etc.
|
|
48
|
+
# Inside double quotes, escape \ and "
|
|
49
|
+
escaped_password = db_config.password.replace('\\', '\\\\').replace('"', '\\"')
|
|
50
|
+
config_content = "[client]\n"
|
|
51
|
+
config_content += f"user={db_config.user}\n"
|
|
52
|
+
config_content += f'password="{escaped_password}"\n'
|
|
53
|
+
if db_config.host:
|
|
54
|
+
config_content += f"host={db_config.host}\n"
|
|
55
|
+
if db_config.port:
|
|
56
|
+
config_content += f"port={db_config.port}\n"
|
|
57
|
+
|
|
58
|
+
random_suffix = secrets.token_hex(8)
|
|
59
|
+
config_path = f"/tmp/.my_{random_suffix}.cnf"
|
|
60
|
+
|
|
61
|
+
if mode.is_remote(client):
|
|
62
|
+
# For remote clients, create config file on remote system
|
|
63
|
+
# Using base64 encoding to safely handle special characters in passwords
|
|
64
|
+
encoded_content = base64.b64encode(config_content.encode()).decode()
|
|
65
|
+
# Use force_output=True to ensure command completes before proceeding
|
|
66
|
+
result = mode.run_command(
|
|
67
|
+
f"echo '{encoded_content}' | base64 -d > {config_path} && chmod 600 {config_path} && echo 'OK'",
|
|
68
|
+
client,
|
|
69
|
+
force_output=True,
|
|
70
|
+
skip_dry_run=True
|
|
71
|
+
)
|
|
72
|
+
result = result.strip() if result else ''
|
|
73
|
+
if result != 'OK':
|
|
74
|
+
output.message(
|
|
75
|
+
output.Subject.WARNING,
|
|
76
|
+
f'Failed to create MySQL config file on remote: {result}',
|
|
77
|
+
True
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
# For local clients, write directly
|
|
81
|
+
with open(config_path, 'w') as f:
|
|
82
|
+
f.write(config_content)
|
|
83
|
+
os.chmod(config_path, 0o600)
|
|
84
|
+
|
|
85
|
+
_mysql_config_files[client] = config_path
|
|
86
|
+
return config_path
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_mysql_config_path(client: str) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Get the MySQL config file path for a client, creating it if necessary.
|
|
92
|
+
|
|
93
|
+
:param client: String client identifier
|
|
94
|
+
:return: String path to the config file
|
|
28
95
|
"""
|
|
29
|
-
|
|
96
|
+
if client not in _mysql_config_files:
|
|
97
|
+
create_mysql_config_file(client)
|
|
98
|
+
return _mysql_config_files[client]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cleanup_mysql_config_files() -> None:
|
|
102
|
+
"""
|
|
103
|
+
Remove all temporary MySQL config files.
|
|
104
|
+
Should be called during cleanup phase.
|
|
105
|
+
"""
|
|
106
|
+
global _mysql_config_files
|
|
107
|
+
|
|
108
|
+
for client, config_path in _mysql_config_files.items():
|
|
109
|
+
try:
|
|
110
|
+
if mode.is_remote(client):
|
|
111
|
+
mode.run_command(
|
|
112
|
+
f"rm -f {config_path}",
|
|
113
|
+
client,
|
|
114
|
+
allow_fail=True,
|
|
115
|
+
skip_dry_run=True
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
if os.path.exists(config_path):
|
|
119
|
+
os.remove(config_path)
|
|
120
|
+
except Exception:
|
|
121
|
+
# Silently ignore cleanup errors
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
_mysql_config_files = {}
|
|
30
125
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
126
|
+
|
|
127
|
+
def run_database_command(client: str, command: str, force_database_name: bool = False) -> str:
|
|
128
|
+
"""
|
|
129
|
+
Run a database command using the "mysql -e" command
|
|
130
|
+
:param client: Client identifier
|
|
131
|
+
:param command: Database command
|
|
132
|
+
:param force_database_name: Forces the database name
|
|
133
|
+
:return: Command output
|
|
134
|
+
"""
|
|
135
|
+
_database_name = ''
|
|
136
|
+
if force_database_name:
|
|
137
|
+
cfg = system.get_typed_config()
|
|
138
|
+
_database_name = ' ' + helper.quote_shell_arg(cfg.get_client(client).db.name)
|
|
139
|
+
|
|
140
|
+
# Escape the SQL command for shell
|
|
141
|
+
# - Backslashes need doubling
|
|
142
|
+
# - Double quotes need escaping
|
|
143
|
+
# - Backticks need escaping (shell command substitution)
|
|
144
|
+
_safe_command = command.replace('\\', '\\\\').replace('"', '\\"').replace('`', '\\`')
|
|
145
|
+
|
|
146
|
+
result = mode.run_command(
|
|
147
|
+
helper.get_command(client, 'mysql') + ' ' + generate_mysql_credentials(
|
|
148
|
+
client) + _database_name + ' -e "' + _safe_command + '"',
|
|
34
149
|
client, True)
|
|
150
|
+
return result if result else ''
|
|
151
|
+
|
|
35
152
|
|
|
153
|
+
def run_sql_batch_with_fk_disabled(client: str, statements: list[str]) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Execute multiple SQL statements in one roundtrip with FK checks disabled.
|
|
156
|
+
DRY helper for batch operations like TRUNCATE or DROP.
|
|
36
157
|
|
|
37
|
-
|
|
158
|
+
:param client: Client identifier
|
|
159
|
+
:param statements: List of SQL statements (without trailing semicolons)
|
|
160
|
+
"""
|
|
161
|
+
if not statements:
|
|
162
|
+
return
|
|
163
|
+
sql = 'SET FOREIGN_KEY_CHECKS = 0; ' + '; '.join(statements) + '; SET FOREIGN_KEY_CHECKS = 1;'
|
|
164
|
+
run_database_command(client, sql, True)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def generate_database_dump_filename() -> None:
|
|
38
168
|
"""
|
|
39
169
|
Generate a database dump filename like "_[name]_[date].sql" or using the give filename
|
|
40
|
-
:return:
|
|
41
170
|
"""
|
|
42
171
|
global database_dump_file_name
|
|
43
172
|
|
|
44
|
-
|
|
173
|
+
cfg = system.get_typed_config()
|
|
174
|
+
if cfg.dump_name == '':
|
|
45
175
|
# _project-db_2022-08-22_12-37.sql
|
|
46
176
|
_now = datetime.datetime.now()
|
|
47
|
-
database_dump_file_name = '_' +
|
|
177
|
+
database_dump_file_name = '_' + cfg.origin.db.name + '_' + _now.strftime(
|
|
48
178
|
"%Y-%m-%d_%H-%M") + '.sql'
|
|
49
179
|
else:
|
|
50
|
-
database_dump_file_name =
|
|
180
|
+
database_dump_file_name = cfg.dump_name + '.sql'
|
|
51
181
|
|
|
52
182
|
|
|
53
|
-
def truncate_tables():
|
|
183
|
+
def truncate_tables() -> None:
|
|
54
184
|
"""
|
|
55
|
-
|
|
56
|
-
# ToDo: Too much conditional nesting
|
|
57
|
-
:return: String
|
|
185
|
+
Truncate specified tables before import using batch operation
|
|
58
186
|
"""
|
|
59
|
-
|
|
60
|
-
if 'truncate_table' in system.config:
|
|
61
|
-
system.config['truncate_tables'] = system.config['truncate_table']
|
|
187
|
+
cfg = system.get_typed_config()
|
|
62
188
|
|
|
63
|
-
if
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
189
|
+
if not cfg.truncate_tables:
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
output.message(
|
|
193
|
+
output.Subject.TARGET,
|
|
194
|
+
'Truncating tables before import',
|
|
195
|
+
True
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Collect all tables to truncate (80-90% fewer network roundtrips)
|
|
199
|
+
tables_to_truncate = []
|
|
200
|
+
for _table in cfg.truncate_tables:
|
|
201
|
+
if '*' in _table:
|
|
202
|
+
_wildcard_tables = get_database_tables_like(mode.Client.TARGET,
|
|
203
|
+
_table.replace('*', '%'))
|
|
204
|
+
if _wildcard_tables:
|
|
205
|
+
tables_to_truncate.extend(_wildcard_tables)
|
|
206
|
+
else:
|
|
207
|
+
# Check if table exists (MariaDB doesn't support IF EXISTS)
|
|
208
|
+
_existing_tables = get_database_tables_like(mode.Client.TARGET, _table)
|
|
209
|
+
if _existing_tables:
|
|
210
|
+
tables_to_truncate.append(_table)
|
|
211
|
+
|
|
212
|
+
if not tables_to_truncate:
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
# Build and execute TRUNCATE statements
|
|
216
|
+
statements = [f'TRUNCATE TABLE {sanitize_table_name(t)}' for t in tables_to_truncate]
|
|
217
|
+
run_sql_batch_with_fk_disabled(mode.Client.TARGET, statements)
|
|
80
218
|
|
|
81
219
|
|
|
82
|
-
def generate_ignore_database_tables():
|
|
220
|
+
def generate_ignore_database_tables() -> str:
|
|
83
221
|
"""
|
|
84
222
|
Generate the ignore tables options for the mysqldump command by the given table list
|
|
85
|
-
|
|
86
|
-
:return: String
|
|
223
|
+
:return: String of ignore table options
|
|
87
224
|
"""
|
|
88
|
-
|
|
89
|
-
if 'ignore_table' in system.config:
|
|
90
|
-
system.config['ignore_tables'] = system.config['ignore_table']
|
|
225
|
+
cfg = system.get_typed_config()
|
|
91
226
|
|
|
92
|
-
_ignore_tables = []
|
|
93
|
-
if
|
|
94
|
-
for table in
|
|
227
|
+
_ignore_tables: list[str] = []
|
|
228
|
+
if cfg.ignore_tables:
|
|
229
|
+
for table in cfg.ignore_tables:
|
|
95
230
|
if '*' in table:
|
|
96
231
|
_wildcard_tables = get_database_tables_like(mode.Client.ORIGIN,
|
|
97
232
|
table.replace('*', '%'))
|
|
@@ -105,110 +240,192 @@ def generate_ignore_database_tables():
|
|
|
105
240
|
return ''
|
|
106
241
|
|
|
107
242
|
|
|
108
|
-
def generate_ignore_database_table(ignore_tables, table):
|
|
243
|
+
def generate_ignore_database_table(ignore_tables: list[str], table: str) -> list[str]:
|
|
109
244
|
"""
|
|
110
|
-
:param ignore_tables:
|
|
111
|
-
:param table:
|
|
112
|
-
:return:
|
|
245
|
+
:param ignore_tables: List of ignore table options
|
|
246
|
+
:param table: Table name to add
|
|
247
|
+
:return: Updated list of ignore table options
|
|
113
248
|
"""
|
|
114
|
-
|
|
249
|
+
cfg = system.get_typed_config()
|
|
250
|
+
# Validate table name to prevent injection
|
|
251
|
+
_safe_table = sanitize_table_name(table)
|
|
252
|
+
# Remove backticks for mysqldump --ignore-table option (it doesn't use them)
|
|
253
|
+
_table_name = _safe_table.strip('`')
|
|
254
|
+
# Validate database name (same rules as table names)
|
|
255
|
+
_safe_db = sanitize_table_name(cfg.origin.db.name)
|
|
256
|
+
_db_name = _safe_db.strip('`')
|
|
257
|
+
ignore_tables.append(f'--ignore-table={_db_name}.{_table_name}')
|
|
115
258
|
return ignore_tables
|
|
116
259
|
|
|
117
260
|
|
|
118
|
-
def get_database_tables_like(client, name):
|
|
261
|
+
def get_database_tables_like(client: str, name: str) -> list[str] | None:
|
|
119
262
|
"""
|
|
120
263
|
Get database table names like the given name
|
|
121
|
-
:param client:
|
|
122
|
-
:param name:
|
|
123
|
-
:return:
|
|
264
|
+
:param client: Client identifier
|
|
265
|
+
:param name: Pattern (may contain % wildcard)
|
|
266
|
+
:return: List of table names or None
|
|
124
267
|
"""
|
|
125
|
-
|
|
126
|
-
|
|
268
|
+
cfg = system.get_typed_config()
|
|
269
|
+
_dbname = cfg.get_client(client).db.name
|
|
270
|
+
# Validate database name to prevent SQL injection
|
|
271
|
+
_safe_dbname = sanitize_table_name(_dbname)
|
|
272
|
+
# Escape single quotes in the pattern to prevent SQL injection
|
|
273
|
+
_safe_pattern = name.replace("'", "''")
|
|
274
|
+
_tables = run_database_command(client, f'SHOW TABLES FROM {_safe_dbname} LIKE \'{_safe_pattern}\';').strip()
|
|
127
275
|
if _tables != '':
|
|
128
276
|
return _tables.split('\n')[1:]
|
|
129
|
-
return
|
|
277
|
+
return None
|
|
130
278
|
|
|
131
279
|
|
|
132
|
-
def get_database_tables():
|
|
280
|
+
def get_database_tables() -> str:
|
|
133
281
|
"""
|
|
134
282
|
Generate specific tables for export
|
|
135
|
-
:return: String
|
|
283
|
+
:return: String of table names
|
|
136
284
|
"""
|
|
137
|
-
|
|
285
|
+
cfg = system.get_typed_config()
|
|
286
|
+
if cfg.tables == '':
|
|
138
287
|
return ''
|
|
139
288
|
|
|
140
289
|
_result = ' '
|
|
141
|
-
_tables =
|
|
290
|
+
_tables = cfg.tables.split(',')
|
|
142
291
|
for _table in _tables:
|
|
143
|
-
|
|
292
|
+
# Validate table name to prevent injection
|
|
293
|
+
_safe_table = sanitize_table_name(_table.strip())
|
|
294
|
+
# Use backtick-quoted name for shell command
|
|
295
|
+
_result += _safe_table + ' '
|
|
144
296
|
return _result
|
|
145
297
|
|
|
146
298
|
|
|
147
|
-
def generate_mysql_credentials(client, force_password=
|
|
299
|
+
def generate_mysql_credentials(client: str, force_password: bool = True) -> str:
|
|
148
300
|
"""
|
|
149
|
-
Generate the needed database credential information for the mysql command
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
301
|
+
Generate the needed database credential information for the mysql command.
|
|
302
|
+
Uses --defaults-extra-file to prevent passwords from appearing in process lists
|
|
303
|
+
while preserving system MySQL configuration (including SSL settings).
|
|
304
|
+
|
|
305
|
+
:param client: Client identifier
|
|
306
|
+
:param force_password: Kept for backwards compatibility, now always uses secure method
|
|
307
|
+
:return: MySQL credentials argument
|
|
153
308
|
"""
|
|
154
|
-
|
|
309
|
+
try:
|
|
310
|
+
config_path = get_mysql_config_path(client)
|
|
311
|
+
# Note: --defaults-extra-file must NOT have quotes around the path
|
|
312
|
+
# mysqldump/mysql parse this option specially
|
|
313
|
+
# Using --defaults-extra-file (not --defaults-file) preserves system config
|
|
314
|
+
credentials = f"--defaults-extra-file={config_path}"
|
|
315
|
+
|
|
316
|
+
cfg = system.get_typed_config()
|
|
317
|
+
if cfg.verbose:
|
|
318
|
+
output.message(
|
|
319
|
+
output.host_to_subject(client),
|
|
320
|
+
f'Using secure credentials file: {config_path}',
|
|
321
|
+
verbose_only=True
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return credentials
|
|
325
|
+
except Exception as e:
|
|
326
|
+
# Fallback to legacy method if config file creation fails
|
|
327
|
+
output.message(
|
|
328
|
+
output.Subject.WARNING,
|
|
329
|
+
f'Falling back to legacy credentials (config file failed: {e})',
|
|
330
|
+
True
|
|
331
|
+
)
|
|
332
|
+
return _generate_mysql_credentials_legacy(client, force_password)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _generate_mysql_credentials_legacy(client: str, force_password: bool = True) -> str:
|
|
336
|
+
"""
|
|
337
|
+
Legacy method: Generate MySQL credentials as command line arguments.
|
|
338
|
+
WARNING: This exposes passwords in process lists!
|
|
339
|
+
|
|
340
|
+
:param client: Client identifier
|
|
341
|
+
:param force_password: Include password in credentials
|
|
342
|
+
:return: MySQL credentials arguments
|
|
343
|
+
"""
|
|
344
|
+
cfg = system.get_typed_config()
|
|
345
|
+
db_cfg = cfg.get_client(client).db
|
|
346
|
+
_credentials = '-u\'' + db_cfg.user + '\''
|
|
155
347
|
if force_password:
|
|
156
|
-
_credentials += ' -p\'' +
|
|
157
|
-
if
|
|
158
|
-
_credentials += ' -h\'' +
|
|
159
|
-
if
|
|
160
|
-
_credentials += ' -P\'' + str(
|
|
348
|
+
_credentials += ' -p\'' + db_cfg.password + '\''
|
|
349
|
+
if db_cfg.host:
|
|
350
|
+
_credentials += ' -h\'' + db_cfg.host + '\''
|
|
351
|
+
if db_cfg.port:
|
|
352
|
+
_credentials += ' -P\'' + str(db_cfg.port) + '\''
|
|
161
353
|
return _credentials
|
|
162
354
|
|
|
163
355
|
|
|
164
|
-
def
|
|
356
|
+
def get_dump_file_path(client: str) -> str:
|
|
165
357
|
"""
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
:
|
|
358
|
+
Get the path to the dump file (without .gz extension).
|
|
359
|
+
DRY helper for consistent path construction.
|
|
360
|
+
|
|
361
|
+
:param client: Client identifier
|
|
362
|
+
:return: Path to dump file
|
|
170
363
|
"""
|
|
171
|
-
if
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
client,
|
|
175
|
-
True,
|
|
176
|
-
skip_dry_run=True
|
|
177
|
-
)
|
|
364
|
+
if database_dump_file_name is None:
|
|
365
|
+
raise DbSyncError('database_dump_file_name not initialized')
|
|
366
|
+
return helper.get_dump_dir(client) + database_dump_file_name
|
|
178
367
|
|
|
179
|
-
if not _line:
|
|
180
|
-
return
|
|
181
368
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
output.message(
|
|
192
|
-
output.host_to_subject(client),
|
|
193
|
-
'Dump file is valid',
|
|
194
|
-
verbose_only=True
|
|
195
|
-
)
|
|
369
|
+
def get_dump_gz_path(client: str) -> str:
|
|
370
|
+
"""
|
|
371
|
+
Get the path to the compressed dump file (.gz).
|
|
372
|
+
DRY helper for consistent path construction.
|
|
373
|
+
|
|
374
|
+
:param client: Client identifier
|
|
375
|
+
:return: Path to compressed dump file
|
|
376
|
+
"""
|
|
377
|
+
return get_dump_file_path(client) + '.gz'
|
|
196
378
|
|
|
197
379
|
|
|
198
|
-
def
|
|
380
|
+
def get_dump_cat_command(client: str, filepath: str) -> str:
|
|
381
|
+
"""
|
|
382
|
+
Get the appropriate command to read a dump file (handles .gz compression).
|
|
383
|
+
DRY helper for check_database_dump and count_tables.
|
|
384
|
+
|
|
385
|
+
:param client: Client identifier
|
|
386
|
+
:param filepath: Path to dump file
|
|
387
|
+
:return: Command prefix for reading the file
|
|
388
|
+
"""
|
|
389
|
+
_safe_filepath = helper.quote_shell_arg(filepath)
|
|
390
|
+
if filepath.endswith('.gz'):
|
|
391
|
+
return f'{helper.get_command(client, "gunzip")} -c {_safe_filepath}'
|
|
392
|
+
return f'{helper.get_command(client, "cat")} {_safe_filepath}'
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def check_database_dump(client: str, filepath: str) -> None:
|
|
396
|
+
"""
|
|
397
|
+
Checking the last line of the dump file if it contains "-- Dump completed on"
|
|
398
|
+
:param client: Client identifier
|
|
399
|
+
:param filepath: Path to dump file
|
|
400
|
+
"""
|
|
401
|
+
cfg = system.get_typed_config()
|
|
402
|
+
if not cfg.check_dump:
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
_cmd = f'{get_dump_cat_command(client, filepath)} | tail -n 1'
|
|
406
|
+
_line = mode.run_command(_cmd, client, True, skip_dry_run=True)
|
|
407
|
+
|
|
408
|
+
if not _line:
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
if "-- Dump completed on" not in _line:
|
|
412
|
+
raise DbSyncError('Dump file is corrupted')
|
|
413
|
+
output.message(
|
|
414
|
+
output.host_to_subject(client),
|
|
415
|
+
'Dump file is valid',
|
|
416
|
+
verbose_only=True
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def count_tables(client: str, filepath: str) -> None:
|
|
199
421
|
"""
|
|
200
422
|
Count the reference string in the database dump file to get the count of all exported tables
|
|
201
|
-
:param client:
|
|
202
|
-
:param filepath:
|
|
203
|
-
:return:
|
|
423
|
+
:param client: Client identifier
|
|
424
|
+
:param filepath: Path to dump file
|
|
204
425
|
"""
|
|
205
426
|
_reference = 'CREATE TABLE'
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
client,
|
|
209
|
-
True,
|
|
210
|
-
skip_dry_run=True
|
|
211
|
-
)
|
|
427
|
+
_cmd = f'{get_dump_cat_command(client, filepath)} | grep -ao "{_reference}" | wc -l | xargs'
|
|
428
|
+
_count = mode.run_command(_cmd, client, True, skip_dry_run=True)
|
|
212
429
|
|
|
213
430
|
if _count:
|
|
214
431
|
output.message(
|
|
@@ -217,11 +434,11 @@ def count_tables(client, filepath):
|
|
|
217
434
|
)
|
|
218
435
|
|
|
219
436
|
|
|
220
|
-
def get_database_version(client):
|
|
437
|
+
def get_database_version(client: str) -> tuple[str | None, str | None]:
|
|
221
438
|
"""
|
|
222
439
|
Check the database version and distinguish between mysql and mariadb
|
|
223
|
-
:param client:
|
|
224
|
-
:return: Tuple
|
|
440
|
+
:param client: Client identifier
|
|
441
|
+
:return: Tuple of (database_system, version_number)
|
|
225
442
|
"""
|
|
226
443
|
_database_system = None
|
|
227
444
|
_version_number = None
|
|
@@ -229,7 +446,8 @@ def get_database_version(client):
|
|
|
229
446
|
_database_version = run_database_command(client, 'SELECT VERSION();').splitlines()[1]
|
|
230
447
|
_database_system = DatabaseSystem.MYSQL
|
|
231
448
|
|
|
232
|
-
|
|
449
|
+
_version_match = re.search(r'(\d+\.)?(\d+\.)?(\*|\d+)', _database_version)
|
|
450
|
+
_version_number = _version_match.group() if _version_match else None
|
|
233
451
|
|
|
234
452
|
if DatabaseSystem.MARIADB.lower() in _database_version.lower():
|
|
235
453
|
_database_system = DatabaseSystem.MARIADB
|