|
28 | 28 | import os
|
29 | 29 | import re
|
30 | 30 | import sys
|
| 31 | +from itertools import product |
| 32 | +from urllib.parse import urljoin |
31 | 33 |
|
32 |
| -import grab |
| 34 | +import requests |
| 35 | +from lxml import html |
33 | 36 |
|
34 |
| -__version__ = '2019-05-07.1' |
| 37 | +__version__ = '2024-12-01' |
35 | 38 |
|
36 | 39 | CONTENT_DISPOSITION_FILENAME_RE = re.compile(r'^.*filename="(?P<filename>[^"]+)".*$')
|
37 | 40 | DEFAULT_PREFIX_FORMAT = r'%Y-%m-%d--%H-%M-%S-UTC_'
|
38 | 41 |
|
39 | 42 |
|
40 |
| -def is_login_successful(g): |
41 |
| -return g.doc.text_search("frame_content") or g.doc.text_search("server_export.php") |
| 43 | +def is_login_successful(tree): |
| 44 | +hrefs = tree.xpath("//a/@href") |
| 45 | +target_substrings = ["frame_content", "server_export.php", "index.php?route=/server/export"] |
| 46 | +combinations = product(target_substrings, hrefs) |
42 | 47 |
|
43 |
| - |
44 |
| -def open_frame_if_phpmyadmin_3(g): |
45 |
| -frame_url_selector = g.doc.select("id('frame_content')/@src") |
46 |
| -if frame_url_selector.exists(): |
47 |
| -g.go(frame_url_selector.text()) |
| 48 | +return any(substring in href for substring, href in combinations) |
48 | 49 |
|
49 | 50 |
|
50 | 51 | def download_sql_backup(url, user, password, dry_run=False, overwrite_existing=False, prepend_date=True, basename=None,
|
51 | 52 | output_directory=os.getcwd(), exclude_dbs=None, compression='none', prefix_format=None,
|
52 | 53 | timeout=60, http_auth=None, server_name=None, **kwargs):
|
53 | 54 | prefix_format = prefix_format or DEFAULT_PREFIX_FORMAT
|
54 |
| -exclude_dbs = exclude_dbs.split(',') or [] |
55 |
| -encoding = '' if compression == 'gzip' else 'gzip' |
56 |
| - |
57 |
| -g = grab.Grab(encoding=encoding, timeout=timeout) |
58 |
| -if http_auth: |
59 |
| -g.setup(userpwd=http_auth) |
60 |
| -g.go(url) |
61 |
| - |
62 |
| -g.doc.set_input_by_id('input_username', user) |
63 |
| -g.doc.set_input_by_id('input_password', password) |
64 |
| -if server_name: |
65 |
| -g.doc.set_input_by_id('input_servername', server_name) |
66 |
| -g.submit() |
67 |
| - |
68 |
| -if not is_login_successful(g): |
69 |
| -raise ValueError('Could not login - did you provide the correct username / password?') |
70 |
| - |
71 |
| -open_frame_if_phpmyadmin_3(g) |
72 |
| - |
73 |
| -export_url = g.doc.select("id('topmenu')//a[contains(@href,'server_export.php')]/@href").text() |
74 |
| -g.go(export_url) |
75 |
| - |
76 |
| -dbs_available = [option.attrib['value'] for option in g.doc.form.inputs['db_select[]']] |
| 55 | +exclude_dbs = exclude_dbs.split(',') if exclude_dbs else [] |
| 56 | +session = requests.Session() |
| 57 | + |
| 58 | +# Login |
| 59 | +response = session.get(url, timeout=timeout) |
| 60 | +if response.status_code != 200: |
| 61 | +raise ValueError("Failed to load the login page.") |
| 62 | + |
| 63 | +tree = html.fromstring(response.content) |
| 64 | +form_action = tree.xpath("//form[@id='login_form']/@action") |
| 65 | +form_action = form_action[0] if form_action else url |
| 66 | + |
| 67 | +form_data = { |
| 68 | +"pma_username": user, |
| 69 | +"pma_password": password, |
| 70 | +} |
| 71 | + |
| 72 | +hidden_inputs = tree.xpath("//form[@id='login_form']//input[@type='hidden']") |
| 73 | +for hidden_input in hidden_inputs: |
| 74 | +name = hidden_input.get("name") |
| 75 | +value = hidden_input.get("value", "") |
| 76 | +if name: |
| 77 | +form_data[name] = value |
| 78 | + |
| 79 | +login_response = session.post(urljoin(url,form_action), data=form_data, timeout=timeout) |
| 80 | + |
| 81 | +if login_response.status_code != 200: |
| 82 | +raise ValueError("Could not log in. Please check your credentials.") |
| 83 | + |
| 84 | +tree = html.fromstring(login_response.content) |
| 85 | +if not is_login_successful(tree): |
| 86 | +raise ValueError("Could not log in. Please check your credentials.") |
| 87 | + |
| 88 | +# Extract export URL |
| 89 | +export_url = tree.xpath("id('topmenu')//a[contains(@href,'server_export.php')]/@href") |
| 90 | +if not export_url: |
| 91 | +export_url = tree.xpath("id('topmenu')//a[contains(@href,'index.php?route=/server/export')]/@href") |
| 92 | +if not export_url: |
| 93 | +raise ValueError("Could not find export URL.") |
| 94 | +export_url = export_url[0] |
| 95 | + |
| 96 | +# Access export page |
| 97 | +export_response = session.get(urljoin(url,export_url), timeout=timeout) |
| 98 | +export_tree = html.fromstring(export_response.content) |
| 99 | + |
| 100 | + |
| 101 | +# Determine databases to dump |
| 102 | +dbs_available = export_tree.xpath("//select[@name='db_select[]']/option/@value") |
77 | 103 | dbs_to_dump = [db_name for db_name in dbs_available if db_name not in exclude_dbs]
|
78 | 104 | if not dbs_to_dump:
|
79 |
| -print('Warning: no databases to dump (databases available: "{}")'.format('", "'.join(dbs_available)), |
80 |
| -file=sys.stderr) |
81 |
| - |
82 |
| -file_response = g.submit( |
83 |
| -extra_post=[('db_select[]', db_name) for db_name in dbs_to_dump] + [('compression', compression)]) |
84 |
| - |
85 |
| -re_match = CONTENT_DISPOSITION_FILENAME_RE.match(g.doc.headers['Content-Disposition']) |
| 105 | +print(f'Warning: no databases to dump (databases available: "{", ".join(dbs_available)}")', |
| 106 | +file=sys.stderr) |
| 107 | + |
| 108 | +# Prepare form data |
| 109 | +dump_form_action = export_tree.xpath("//form[@name='dump']/@action")[0] |
| 110 | +form_data = {'db_select[]': dbs_to_dump} |
| 111 | +form_data['compression'] = compression |
| 112 | +form_data['what'] = 'sql' |
| 113 | +form_data['filename_template'] = '@SERVER@' |
| 114 | +form_data['sql_structure_or_data'] = 'structure_and_data' |
| 115 | +dump_hidden_inputs = export_tree.xpath("//form[@name='dump']//input[@type='hidden']") |
| 116 | +for hidden_input in dump_hidden_inputs: |
| 117 | +name = hidden_input.get("name") |
| 118 | +value = hidden_input.get("value", "") |
| 119 | +if name: |
| 120 | +form_data[name] = value |
| 121 | + |
| 122 | +# Submit form and download file |
| 123 | +file_response = session.post(urljoin(url, dump_form_action), data=form_data, timeout=timeout, stream=True) |
| 124 | +content_disposition = file_response.headers.get('Content-Disposition', '') |
| 125 | +re_match = CONTENT_DISPOSITION_FILENAME_RE.match(content_disposition) |
86 | 126 | if not re_match:
|
87 |
| -raise ValueError( |
88 |
| -'Could not determine SQL backup filename from {}'.format(g.doc.headers['Content-Disposition'])) |
| 127 | +raise ValueError(f"Could not determine SQL backup filename from {content_disposition}") |
89 | 128 |
|
90 | 129 | content_filename = re_match.group('filename')
|
91 | 130 | filename = content_filename if basename is None else basename + os.path.splitext(content_filename)[1]
|
@@ -97,16 +136,19 @@ def download_sql_backup(url, user, password, dry_run=False, overwrite_existing=F
|
97 | 136 | if os.path.isfile(out_filename) and not overwrite_existing:
|
98 | 137 | basename, ext = os.path.splitext(out_filename)
|
99 | 138 | n = 1
|
100 |
| -print('File {} already exists, to overwrite it use --overwrite-existing'.format(out_filename), file=sys.stderr) |
| 139 | +print(f'File {out_filename} already exists, to overwrite it use --overwrite-existing', file=sys.stderr) |
101 | 140 | while True:
|
102 |
| -alternate_out_filename = '{}_({}){}'.format(basename, n, ext) |
| 141 | +alternate_out_filename = f'{basename}_({n}){ext}' |
103 | 142 | if not os.path.isfile(alternate_out_filename):
|
104 | 143 | out_filename = alternate_out_filename
|
105 | 144 | break
|
106 | 145 | n += 1
|
107 | 146 |
|
| 147 | +# Save file if not dry run |
108 | 148 | if not dry_run:
|
109 |
| -file_response.save(out_filename) |
| 149 | +with open(out_filename, 'wb') as f: |
| 150 | +for chunk in file_response.iter_content(chunk_size=8192): |
| 151 | +f.write(chunk) |
110 | 152 |
|
111 | 153 | return out_filename
|
112 | 154 |
|
|
0 commit comments