PluginProbe ʕ •ᴥ•ʔ
JetBackup – Backup, Restore & Migrate / 3.1.13.4
JetBackup – Backup, Restore & Migrate v3.1.13.4
3.1.22.3 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.8.1 1.4.9 1.5.0 1.5.1 1.5.1.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 1.6.10 1.6.11 1.6.12 1.6.13 1.6.15 1.6.5.1 1.6.8.8 1.6.9 1.6.9.1 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7.5 2.0.8.7 2.0.9.11 2.0.9.14 2.0.9.15 2.0.9.6 2.0.9.7 2.0.9.9 3.1.10.7 3.1.11.1 3.1.12.3 3.1.13.4 3.1.14.17 3.1.15.4 3.1.16.1 3.1.17.5 3.1.18.10 3.1.18.8 3.1.18.9 3.1.19.8 3.1.20.3 3.1.21.3 3.1.7.9 3.1.9.2 trunk 1.1.90 1.1.91 1.2.0 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2
backup / src / JetBackup / Data / Mysqldump.php
backup / src / JetBackup / Data Last commit date
.htaccess 9 months ago ArrayData.php 9 months ago DBObject.php 9 months ago Engine.php 9 months ago Mysqldump.php 9 months ago ReflectionObject.php 9 months ago SleekStore.php 9 months ago index.html 9 months ago web.config 9 months ago
Mysqldump.php
361 lines
1 <?php
2
3 namespace JetBackup\Data;
4
5 use Exception;
6 use JetBackup\Factory;
7 use JetBackup\Log\LogController;
8 use Mysqldump\Mysqldump as MysqldumpAlias;
9 use PDO;
10 use PDOException;
11
12 if (!defined( '__JETBACKUP__')) die('Direct access is not allowed');
13
14 Class Mysqldump Extends MysqldumpAlias {
15
16 private ArrayData $_data;
17 private LogController $_logController;
18
19 const QUERY_MAX_RETRIES = 10;
20 const SQLSTATE_ERRORS = ['08S01', '08001', '40001', 'HY000'];
21
22 /***
23 * These SQLSTATE errors indicate connection failures and trigger a retry.
24 * - '08S01' → Communication link failure (e.g., network disconnect, timeout, or dropped connection)
25 * - '08001' → Client unable to establish a connection (e.g., incorrect credentials, DNS issues)
26 * - '40001' → Transaction deadlock detected (e.g., deadlock errors requiring retry)
27 * - 'HY000' → General MySQL error (covers various issues, including MySQL server disconnects)
28 */
29
30
31 /**
32 * @throws Exception
33 */
34 public function __construct($db_name, $db_user, $db_password, $db_host) {
35
36 $this->_data = new ArrayData();
37
38 $this->_setDBName($db_name);
39 $this->_setDBPassword($db_password);
40 $this->_setDBUser($db_user);
41 $this->_setDBHost($db_host);
42
43 parent::__construct(
44 'mysql:host='.$this->getDBHost().';port='.$this->getDBPort().';dbname='.$this->getDBName(), //dsn
45 $this->getDBUser(),
46 $this->getDBPassword(),
47 [
48 'compress' => MysqldumpAlias::NONE, // Always use NONE, we use our own internal gzip class
49 'add-drop-table' => true, // Enable DROP TABLE IF EXISTS
50 'if-not-exists' => true,
51 'reset-auto-increment' => true,//resets the AUTO_INCREMENT value in tables to ensure consistency when restoring data.
52 'complete-insert' => true,// include column names in each INSERT statement
53 'default-character-set' => MysqldumpAlias::UTF8MB4,
54 'extended-insert' => false,// when false, each insert will be in a separate line helping for resuming import
55 'insert-ignore' => true,
56 'lock-tables' => false,// no need to lock if using single-transaction
57 ]
58 );
59
60 }
61
62 public function setDumpSetting($key, $value):void { $this->dumpSettings[$key] = $value; }
63 public function getDumpSetting($key, $default=null) { return $this->dumpSettings[$key] ?? $default; }
64
65 public function set($key, $value){
66 $this->_data->set($key, $value);
67 }
68
69 public function get($key, $default=''){
70 return $this->_data->get($key, $default);
71 }
72
73
74 public function setLogController(LogController $log) {
75 $this->_logController = $log;
76 }
77
78 private function getLogController(): LogController {
79 if (!isset($this->_logController)) $this->_logController = new LogController();
80 return $this->_logController;
81 }
82
83
84 private function _setDBHost($db_host) {
85 if (strpos($db_host, ':') !== false) {
86 $parts = explode(':', $db_host, 2);
87 $db_host = $parts[0] ?? 'localhost';
88 $this->_setDBPort((int) ($parts[1] ?? 3306));
89 }
90 $this->set('db_host', $db_host);
91 }
92
93 public function getDBHost() { return $this->get('db_host'); }
94
95 private function _setDBPort($db_port) { $this->set('db_port', $db_port); }
96 public function getDBPort() { return $this->get('db_port', Factory::getSettingsGeneral()->getMySQLDefaultPort()); }
97
98 private function _setDBName($db_name) { $this->set('db_name', $db_name); }
99 public function getDBName() { return $this->get('db_name'); }
100
101 private function _setDBUser($db_user) { $this->set('db_user', $db_user); }
102 public function getDBUser() { return $this->get('db_user'); }
103
104 private function _setDBPassword($db_password) { $this->set('db_password', $db_password); }
105 public function getDBPassword() { return $this->get('db_password'); }
106
107 public function setInclude(array $include) {
108 $this->setDumpSetting('include-tables', $include);
109 $this->setDumpSetting('include-views', $include);
110 }
111 public function getInclude() { return $this->getDumpSetting('include-tables', []); }
112
113 public function setExclude($exclude) { $this->setDumpSetting('no-data', $exclude); }
114 public function getExclude() { return $this->getDumpSetting('no-data', []); }
115
116 /**
117 * @param $buffer
118 *
119 * @return mixed|null
120 *
121 * Detect problematic SET commands that might involve '@OLD_' or '@saved_' variables
122 */
123 private function _checkResume($buffer) {
124
125 $problematicSetPattern = '/SET\s+\w+\s*=\s*@[\w_]+/i';
126
127 if (preg_match($problematicSetPattern, $buffer)) {
128 $this->getLogController()->logMessage("Skipping problematic statement: {$buffer}");
129 return null; // Skip this query
130 }
131
132 return $buffer;
133 }
134
135
136
137 /**
138 * Override connect method with proper retry mechanism.
139 * @throws Exception
140 */
141 protected function connect() {
142
143 $attempts = 0;
144 $waitTime = 500000; // Start with 500ms
145
146 while ($attempts < self::QUERY_MAX_RETRIES) {
147 try {
148 $this->getLogController()->logDebug("Attempting to reconnect to MySQL (Attempt #$attempts)");
149
150 // Call the parent connect method
151 parent::connect();
152
153 // If connection is successful, return
154 if ($this->dbHandler) {
155 $this->getLogController()->logDebug("Successfully reconnected to MySQL.");
156 return;
157 }
158
159 } catch (Exception $e) {
160 $this->getLogController()->logMessage("Reconnect attempt #$attempts failed (SQLSTATE: {$e->getCode()}): " . $e->getMessage());
161
162 if (in_array($e->getCode(), self::SQLSTATE_ERRORS)) {
163 $this->getLogController()->logMessage("Connection error detected (SQLSTATE: {$e->getCode()}), destroying dbHandler...");
164 $this->dbHandler = null;
165 }
166
167 if ($attempts >= self::QUERY_MAX_RETRIES - 1) {
168 throw new Exception("MySQL reconnect failed after $attempts attempts: " . $e->getMessage());
169 }
170
171 // Wait before retrying
172 usleep($waitTime);
173 $waitTime = min($waitTime * 2, 60000000); // Exponential backoff (max 60s)
174 }
175
176 $attempts++; // Increment attempt counter
177 }
178 }
179
180
181 /**
182 * @throws Exception
183 */
184 private static function AtomicWrite(string $path, string $content): bool
185 {
186 $_swap_file = $path . '.swap';
187
188 try {
189
190 // Write content to a temporary file
191 if (file_put_contents($_swap_file, $content, LOCK_EX) === false) {
192 $error = error_get_last();
193 throw new Exception("Failed to write to temporary file: $_swap_file. Error: " . $error['message']);
194 }
195
196
197 // rename swap to target
198 if (!@rename($_swap_file, $path)) {
199 if (!file_exists($_swap_file) && file_exists($path)) return true;
200 $error = error_get_last();
201 throw new Exception("Failed to rename temporary file to target file: $path. Error: " . $error['message']);
202 }
203
204 return true;
205
206 } catch (Exception $e) {
207 throw new Exception($e->getMessage(), $e->getCode());
208 }
209
210 }
211
212 /**
213 * @throws Exception
214 */
215
216 public function import($path) {
217
218 try {
219
220 if (!$path || !is_file($path)) throw new Exception("[import] File {$path} does not exist.");
221
222 $_table = basename($path);
223 $_progress_file = $path . ".progress";
224 $_progress_position = file_exists($_progress_file) ? (int)file_get_contents($_progress_file) : 0;
225
226 $handle = fopen($path, 'rb');
227 if (!$handle) throw new Exception("Failed reading file {$path}. Check access permissions.");
228
229 if (!$this->dbHandler) $this->connect();
230
231 // SQL mode 'NO_ENGINE_SUBSTITUTION' ensures that MySQL throws an error if the specified storage engine is unavailable,
232 // preventing MySQL from automatically substituting it with the default engine, which helps maintain data integrity and consistency.
233 $this->query_exec("SET sql_mode = 'NO_ENGINE_SUBSTITUTION';");
234
235 // Seek to the last processed position if it exists
236 if ($_progress_position > 0) {
237 fseek($handle, $_progress_position);
238 $this->getLogController()->logMessage("Resuming import for {$_table}, Position: {$_progress_position}");
239 } else {
240 $this->getLogController()->logMessage("Starting new import for: {$_table}");
241 }
242
243 $buffer = '';
244
245 while (!feof($handle)) {
246
247 $line = trim(fgets($handle));
248 $currentPosition = ftell($handle);
249 //$this->_log->write("Processing line at position: {$currentPosition}");
250 if (substr($line, 0, 2) == '--' || !$line) continue; // skip comments
251 $buffer .= $line;
252
253 if (';' == substr(rtrim($line), -1, 1)) {
254 try {
255 $buffer = $this->_checkResume($buffer);
256 // buffer is set to null at _checkResume function, to force skip set variables when resuming
257 if ($buffer !== null) $this->query_exec($buffer);
258 // Update the progress file with the current file position
259 self::AtomicWrite($_progress_file, $currentPosition);
260 if ($currentPosition % 1000 === 0) {
261 $this->getLogController()->logMessage("Importing: $_table : $currentPosition ");
262 }
263 $buffer = '';
264
265 } catch (PDOException $e) {
266
267 $this->getLogController()->logMessage( "Failed to execute query: {$buffer}");
268 $this->getLogController()->logMessage( "Error: " . $e->getMessage());
269 $this->getLogController()->logMessage( "SQLSTATE: " . $e->getCode());
270
271 throw new Exception( "Failed to execute query: {$buffer}");
272
273 }
274 }
275
276 //sleep(1); // debug!
277 }
278
279 fclose($handle);
280 $this->getLogController()->logMessage( "Finished importing {$_table}");
281
282 // Remove the status file after the restore is complete
283 if (is_file($_progress_file)) {
284 @unlink($_progress_file);
285 $this->getLogController()->logMessage( "Progress file for table removed");
286 }
287
288 } catch (Exception $e) {
289 $this->getLogController()->logMessage( "Error: " . $e->getMessage());
290 throw new Exception($e->getMessage());
291 }
292 }
293
294 /**
295 * Execute a query that returns a result set (e.g., SELECT, SHOW TABLES).
296 *
297 * @param string $query The SQL query to execute.
298 *
299 * @return array|null Returns an array of results or null on failure.
300 * @throws Exception
301 */
302 public function query_exec(string $query, array $params = []): ?array {
303
304 $waitTime = 500000;
305 $attempt = 0;
306 $is_write_query = preg_match('/^\s*(INSERT|UPDATE|DELETE|REPLACE)/i', $query); // Detect write queries
307
308 while ($attempt < self::QUERY_MAX_RETRIES) {
309 try {
310 if (!$this->dbHandler) {
311 $this->getLogController()->logMessage("No database handler found, attempting to connect.");
312 $this->connect();
313 }
314
315 // **Start transaction for write queries**
316 if ($is_write_query) {
317 if ($this->dbHandler->inTransaction()) {
318 $this->getLogController()->logMessage("Warning: Already in a transaction, committing previous transaction before starting a new one.");
319 $this->dbHandler->commit();
320 }
321 $this->getLogController()->logMessage("Starting transaction for: " . (strlen($query) > 50 ? substr($query, 0, 47) . "..." : $query));
322 $this->dbHandler->beginTransaction();
323 $this->dbHandler->setAttribute(PDO::ATTR_AUTOCOMMIT, 0);
324 }
325
326 $stmt = $this->dbHandler->prepare($query);
327 $executionResult = $stmt->execute($params);
328
329 if ($executionResult) {
330 if ($is_write_query) {
331 $this->getLogController()->logMessage("Committing transaction for: " . (strlen($query) > 50 ? substr($query, 0, 47) . "..." : $query));
332 $this->dbHandler->commit();
333 $this->dbHandler->setAttribute(PDO::ATTR_AUTOCOMMIT, 1);
334 }
335 return $stmt->fetchAll(PDO::FETCH_OBJ);
336 } else {
337 $this->getLogController()->logMessage("Query execution failed.");
338 return null;
339 }
340
341 } catch (PDOException $e) {
342 if ($is_write_query) {
343 $this->getLogController()->logMessage("Rolling back transaction due to error.");
344 $this->dbHandler->rollBack();
345 }
346
347 $sqlState = $e->getCode();
348 if (in_array($sqlState, self::SQLSTATE_ERRORS)) {
349 $this->getLogController()->logMessage("SQLSTATE $sqlState encountered, reconnecting...");
350 $this->connect();
351 usleep($waitTime);
352 $waitTime = min($waitTime * 2, 60000000);
353 $attempt++;
354 continue;
355 }
356 throw new Exception("Query execution encountered an error: " . $e->getMessage(), $sqlState);
357 }
358 }
359 return null;
360 }
361 }