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
- else :
61
- g .doc .set_input_by_id ('input_username' , user )
62
- g .doc .set_input_by_id ('input_password' , password )
63
- g .submit ()
64
- g .go (url )
65
- if server_name :
66
- g .doc .set_input_by_id ('input_servername' , server_name )
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