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/utility/parser.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: future_fstrings -*-
|
|
3
2
|
|
|
4
3
|
"""
|
|
5
4
|
Parser script
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
7
|
import sys
|
|
8
|
+
import types
|
|
9
9
|
from db_sync_tool.utility import mode, system, output, helper
|
|
10
|
+
from db_sync_tool.utility.exceptions import ConfigError, ValidationError
|
|
10
11
|
from db_sync_tool.remote import client as remote_client
|
|
11
12
|
|
|
12
13
|
|
|
@@ -46,18 +47,18 @@ def get_database_configuration(client):
|
|
|
46
47
|
:param client: String
|
|
47
48
|
:return:
|
|
48
49
|
"""
|
|
49
|
-
|
|
50
|
+
cfg = system.get_typed_config()
|
|
50
51
|
|
|
51
52
|
# check framework type
|
|
52
53
|
_base = ''
|
|
53
54
|
|
|
54
55
|
automatic_type_detection()
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
):
|
|
60
|
-
_type =
|
|
57
|
+
# Re-get config after type detection may have updated it
|
|
58
|
+
cfg = system.get_typed_config()
|
|
59
|
+
|
|
60
|
+
if cfg.type and (cfg.origin.path != '' or cfg.target.path != ''):
|
|
61
|
+
_type = cfg.type.lower()
|
|
61
62
|
if _type == 'typo3':
|
|
62
63
|
# TYPO3 sync base
|
|
63
64
|
_base = Framework.TYPO3
|
|
@@ -74,25 +75,14 @@ def get_database_configuration(client):
|
|
|
74
75
|
# Laravel sync base
|
|
75
76
|
_base = Framework.LARAVEL
|
|
76
77
|
else:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
output.Subject.ERROR,
|
|
80
|
-
f'Framework type not supported: {_type}',
|
|
81
|
-
False
|
|
82
|
-
)
|
|
83
|
-
)
|
|
84
|
-
elif 'db' in system.config['origin'] or 'db' in system.config['target']:
|
|
78
|
+
raise ConfigError(f'Framework type not supported: {_type}')
|
|
79
|
+
elif cfg.origin.db.name != '' or cfg.target.db.name != '':
|
|
85
80
|
_base = Framework.MANUAL
|
|
86
81
|
else:
|
|
87
|
-
|
|
88
|
-
output.message(
|
|
89
|
-
output.Subject.ERROR,
|
|
90
|
-
f'Missing framework type or database credentials',
|
|
91
|
-
False
|
|
92
|
-
)
|
|
93
|
-
)
|
|
82
|
+
raise ConfigError('Missing framework type or database credentials')
|
|
94
83
|
|
|
95
84
|
sys.path.append('../recipes')
|
|
85
|
+
_parser: types.ModuleType | None = None
|
|
96
86
|
if _base == Framework.TYPO3:
|
|
97
87
|
# Import TYPO3 parser
|
|
98
88
|
from ..recipes import typo3
|
|
@@ -143,7 +133,8 @@ def load_parser(client, parser):
|
|
|
143
133
|
:param parser:
|
|
144
134
|
:return:
|
|
145
135
|
"""
|
|
146
|
-
|
|
136
|
+
cfg = system.get_typed_config()
|
|
137
|
+
_path = cfg.get_client(client).path
|
|
147
138
|
|
|
148
139
|
output.message(
|
|
149
140
|
output.host_to_subject(client),
|
|
@@ -163,13 +154,7 @@ def load_parser(client, parser):
|
|
|
163
154
|
|
|
164
155
|
# Check only if database configuration is a file
|
|
165
156
|
if not helper.check_file_exists(client, _path) and _path[-1] != '/':
|
|
166
|
-
|
|
167
|
-
output.message(
|
|
168
|
-
output.Subject.ERROR,
|
|
169
|
-
f'Database configuration for {client} not found: {_path}',
|
|
170
|
-
False
|
|
171
|
-
)
|
|
172
|
-
)
|
|
157
|
+
raise ConfigError(f'Database configuration for {client} not found: {_path}')
|
|
173
158
|
parser.check_configuration(client)
|
|
174
159
|
|
|
175
160
|
|
|
@@ -179,6 +164,9 @@ def validate_database_credentials(client):
|
|
|
179
164
|
:param client: String
|
|
180
165
|
:return:
|
|
181
166
|
"""
|
|
167
|
+
cfg = system.get_typed_config()
|
|
168
|
+
db_cfg = cfg.get_client(client).db
|
|
169
|
+
|
|
182
170
|
output.message(
|
|
183
171
|
output.host_to_subject(client),
|
|
184
172
|
'Validating database credentials',
|
|
@@ -187,21 +175,10 @@ def validate_database_credentials(client):
|
|
|
187
175
|
_db_credential_keys = ['name', 'host', 'password', 'user']
|
|
188
176
|
|
|
189
177
|
for _key in _db_credential_keys:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
f'Missing database credential "{_key}" for {client} client',
|
|
195
|
-
False
|
|
196
|
-
)
|
|
197
|
-
)
|
|
198
|
-
if system.config[client]['db'][_key] is None or system.config[client]['db'][_key] == '':
|
|
199
|
-
sys.exit(
|
|
200
|
-
output.message(
|
|
201
|
-
output.Subject.ERROR,
|
|
202
|
-
f'Missing database credential "{_key}" for {client} client',
|
|
203
|
-
False
|
|
204
|
-
)
|
|
178
|
+
_value = getattr(db_cfg, _key, None)
|
|
179
|
+
if _value is None or _value == '':
|
|
180
|
+
raise ValidationError(
|
|
181
|
+
f'Missing database credential "{_key}" for {client} client'
|
|
205
182
|
)
|
|
206
183
|
else:
|
|
207
184
|
output.message(
|
|
@@ -215,25 +192,28 @@ def automatic_type_detection():
|
|
|
215
192
|
"""
|
|
216
193
|
Detects the framework type by the provided path using the default mapping
|
|
217
194
|
"""
|
|
218
|
-
|
|
219
|
-
|
|
195
|
+
cfg = system.get_typed_config()
|
|
196
|
+
|
|
197
|
+
# Skip if type is already set or manual db config is provided
|
|
198
|
+
if cfg.type or cfg.origin.db.name != '' or cfg.target.db.name != '':
|
|
220
199
|
return
|
|
221
200
|
|
|
222
|
-
|
|
201
|
+
detected_type = None
|
|
223
202
|
file = None
|
|
224
203
|
|
|
225
204
|
for _client in [mode.Client.ORIGIN, mode.Client.TARGET]:
|
|
226
|
-
|
|
227
|
-
|
|
205
|
+
client_cfg = cfg.get_client(_client)
|
|
206
|
+
if client_cfg.path != '':
|
|
207
|
+
file = helper.get_file_from_path(client_cfg.path)
|
|
228
208
|
for _key, _files in mapping.items():
|
|
229
209
|
if file in _files:
|
|
230
|
-
|
|
210
|
+
detected_type = _key
|
|
231
211
|
|
|
232
|
-
if
|
|
212
|
+
if detected_type:
|
|
233
213
|
output.message(
|
|
234
214
|
output.Subject.LOCAL,
|
|
235
215
|
f'Automatic framework type detection '
|
|
236
216
|
f'{output.CliFormat.BLACK}{file}{output.CliFormat.ENDC}',
|
|
237
217
|
verbose_only=True
|
|
238
218
|
)
|
|
239
|
-
system.
|
|
219
|
+
system.set_framework_type(detected_type)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Pure utility functions with no project dependencies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_version(version_output: str | None) -> str | None:
|
|
13
|
+
"""
|
|
14
|
+
Parse version out of console output.
|
|
15
|
+
https://stackoverflow.com/a/60730346
|
|
16
|
+
|
|
17
|
+
:param version_output: Console output string
|
|
18
|
+
:return: Version string or None
|
|
19
|
+
"""
|
|
20
|
+
if not version_output:
|
|
21
|
+
return None
|
|
22
|
+
_version_pattern = r'\d+(=?\.(\d+(=?\.(\d+)*)*)*)*'
|
|
23
|
+
_regex_matcher = re.compile(_version_pattern)
|
|
24
|
+
_version = _regex_matcher.search(version_output)
|
|
25
|
+
if _version:
|
|
26
|
+
return _version.group(0)
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_file_from_path(path: str) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Trims a path string to retrieve the file.
|
|
33
|
+
|
|
34
|
+
:param path: File path
|
|
35
|
+
:return: File name
|
|
36
|
+
"""
|
|
37
|
+
return Path(path).name
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def remove_surrounding_quotes(s: Any) -> Any:
|
|
41
|
+
"""
|
|
42
|
+
Removes the enclosing quotes (single or double),
|
|
43
|
+
if there are quotes at both the beginning and end of the string.
|
|
44
|
+
|
|
45
|
+
:param s: The string to be checked
|
|
46
|
+
:return: The string without enclosing quotes, if available
|
|
47
|
+
"""
|
|
48
|
+
if isinstance(s, str):
|
|
49
|
+
if s.startswith('"') and s.endswith('"'):
|
|
50
|
+
return s[1:-1]
|
|
51
|
+
elif s.startswith("'") and s.endswith("'"):
|
|
52
|
+
return s[1:-1]
|
|
53
|
+
return s
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def clean_db_config(config: dict[str, Any]) -> dict[str, Any]:
|
|
57
|
+
"""
|
|
58
|
+
Iterates over all entries of a dictionary and removes enclosing
|
|
59
|
+
quotes from the values, if present.
|
|
60
|
+
|
|
61
|
+
:param config: The dictionary to be edited
|
|
62
|
+
:return: A new dictionary with adjusted values
|
|
63
|
+
"""
|
|
64
|
+
return {key: remove_surrounding_quotes(value) for key, value in config.items()}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def dict_to_args(data: dict[str, Any]) -> list[str] | None:
|
|
68
|
+
"""
|
|
69
|
+
Convert a dictionary to an args list.
|
|
70
|
+
|
|
71
|
+
:param data: Dictionary to convert
|
|
72
|
+
:return: List of arguments or None if empty
|
|
73
|
+
"""
|
|
74
|
+
args = []
|
|
75
|
+
for key, val in data.items():
|
|
76
|
+
if val is True:
|
|
77
|
+
args.append(f'--{key}')
|
|
78
|
+
elif val is not False and val is not None:
|
|
79
|
+
args.extend([f'--{key}', str(val)])
|
|
80
|
+
return args or None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def remove_multiple_elements_from_string(elements: list, string: str) -> str:
|
|
84
|
+
"""
|
|
85
|
+
Removes multiple elements from a string.
|
|
86
|
+
|
|
87
|
+
:param elements: List of elements to remove
|
|
88
|
+
:param string: Input string
|
|
89
|
+
:return: String with elements removed
|
|
90
|
+
"""
|
|
91
|
+
for element in elements:
|
|
92
|
+
string = string.replace(element, '')
|
|
93
|
+
return string
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Security utility functions.
|
|
5
|
+
|
|
6
|
+
This module contains pure security functions with minimal dependencies,
|
|
7
|
+
making them safe to import without circular import issues.
|
|
8
|
+
Only depends on utility/exceptions.py which has no other project dependencies.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
import shlex
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from db_sync_tool.utility.exceptions import ValidationError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def quote_shell_arg(arg: Any) -> str:
|
|
19
|
+
"""
|
|
20
|
+
Safely quote a string for use as a shell argument.
|
|
21
|
+
Prevents command injection by escaping special characters.
|
|
22
|
+
|
|
23
|
+
:param arg: Value to quote (will be converted to string)
|
|
24
|
+
:return: Safely quoted string
|
|
25
|
+
"""
|
|
26
|
+
if arg is None:
|
|
27
|
+
return "''"
|
|
28
|
+
return shlex.quote(str(arg))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def sanitize_table_name(table: str) -> str:
|
|
32
|
+
"""
|
|
33
|
+
Validate and sanitize a table name to prevent SQL injection.
|
|
34
|
+
MySQL table names can contain alphanumeric chars, underscores, and dollar signs.
|
|
35
|
+
They can also contain hyphens and dots in quoted identifiers.
|
|
36
|
+
|
|
37
|
+
:param table: Table name to sanitize
|
|
38
|
+
:return: Backtick-quoted table name
|
|
39
|
+
:raises ValidationError: If table name contains invalid characters
|
|
40
|
+
"""
|
|
41
|
+
if not table:
|
|
42
|
+
raise ValidationError("Table name cannot be empty")
|
|
43
|
+
|
|
44
|
+
# Allow alphanumeric, underscore, hyphen, dot, dollar sign
|
|
45
|
+
if not re.match(r'^[a-zA-Z0-9_$.-]+$', table):
|
|
46
|
+
raise ValidationError(f"Invalid table name: {table}")
|
|
47
|
+
|
|
48
|
+
return f"`{table}`"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def sanitize_command_for_logging(command: str) -> str:
|
|
52
|
+
"""
|
|
53
|
+
Remove sensitive information from commands before logging.
|
|
54
|
+
This prevents credentials from appearing in verbose output or logs.
|
|
55
|
+
|
|
56
|
+
:param command: Command string to sanitize
|
|
57
|
+
:return: Sanitized command string
|
|
58
|
+
"""
|
|
59
|
+
patterns = [
|
|
60
|
+
# MySQL password patterns
|
|
61
|
+
(r"-p'[^']*'", "-p'***'"),
|
|
62
|
+
(r'-p"[^"]*"', '-p"***"'),
|
|
63
|
+
(r"-p[^\s'\"]+", "-p***"),
|
|
64
|
+
# SSHPASS patterns
|
|
65
|
+
(r"SSHPASS='[^']*'", "SSHPASS='***'"),
|
|
66
|
+
(r'SSHPASS="[^"]*"', 'SSHPASS="***"'),
|
|
67
|
+
(r"SSHPASS=[^\s]+", "SSHPASS=***"),
|
|
68
|
+
# MySQL defaults-file/defaults-extra-file (mask path to prevent disclosure)
|
|
69
|
+
(r"--defaults-file=[^\s]+", "--defaults-file=***"),
|
|
70
|
+
(r"--defaults-extra-file=[^\s]+", "--defaults-extra-file=***"),
|
|
71
|
+
# Base64 encoded credentials
|
|
72
|
+
(r"echo '[A-Za-z0-9+/=]{20,}' \| base64", "echo '***' | base64"),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
sanitized = command
|
|
76
|
+
for pattern, replacement in patterns:
|
|
77
|
+
sanitized = re.sub(pattern, replacement, sanitized)
|
|
78
|
+
|
|
79
|
+
return sanitized
|