PluginProbe ʕ •ᴥ•ʔ
Backup Migration / 1.3.2
Backup Migration v1.3.2
2.1.6 2.1.5.2 trunk 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.6.1 1.4.7 1.4.8 1.4.9 1.4.9.1 2.0.0 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.5.1
backup-backup / includes / database / better-backup-v3.php
backup-backup / includes / database Last commit date
better-backup-v3.php 2 years ago better-backup.php 2 years ago better-restore.php 2 years ago even-better-restore-v3.php 2 years ago even-better-restore-v4.php 2 years ago manager.php 2 years ago search-replace.php 2 years ago smart-sort.php 2 years ago
better-backup-v3.php
628 lines
1 <?php
2
3 /**
4 * Author: Mikołaj `iClyde` Chodorowski
5 * Contact: kontakt@iclyde.pl
6 * Package: Backup Migration – WP Plugin
7 */
8
9 // Namespace
10 namespace BMI\Plugin\Database;
11
12 // Use
13 use BMI\Plugin\BMI_Logger AS Logger;
14 use BMI\Plugin\Progress\BMI_ZipProgress AS Progress;
15 use BMI\Plugin\Dashboard AS Dashboard;
16 use BMI\Plugin\Staging\BMI_Staging as Staging;
17
18 // Exit on direct access
19 if (!defined('ABSPATH')) exit;
20
21 // echo "Memory usage at the beginning: " . (memory_get_usage() / 1024 / 1024) . " MB \n";
22 // function bmi_find_wordpress_base_path() {
23 //
24 // $dir = dirname(__FILE__);
25 // $previous = null;
26 //
27 // do {
28 //
29 // if (file_exists($dir . '/wp-config.php')) return $dir;
30 // if ($previous == $dir) break;
31 // $previous = $dir;
32 //
33 // } while ($dir = dirname($dir));
34 //
35 // return null;
36 //
37 // }
38 //
39 // define('BASE_PATH', bmi_find_wordpress_base_path() . '/');
40 // define('WP_USE_THEMES', false);
41 //
42 // // Use WP Globals and load WordPress
43 // global $wp, $wp_query, $wp_the_query, $wp_rewrite, $wp_did_header;
44 // require_once BASE_PATH . 'wp-load.php';
45 // echo "Memory usage after core load: " . (memory_get_usage() / 1024 / 1024) . " MB \n";
46 // ini_set('memory_limit', '2M');
47
48 /**
49 * Database exporting
50 * Main Class, requires $wpdb
51 */
52 class BMI_Database_Exporter {
53
54 /**
55 * Private local variables
56 */
57 private $total_tables = 0;
58 private $recipes = [];
59 private $tables_by_size = [];
60 public $total_queries = 0;
61 public $total_rows = 0;
62 public $total_size = 0;
63 public $files = [];
64
65 public $wpdb;
66 public $logger;
67 public $storage;
68 public $percentage;
69 public $max_rows;
70 public $max_query_size;
71 public $table_prefix;
72 public $init_start;
73
74 /**
75 * __construct - Initialization and logger resolver
76 *
77 * @return self
78 */
79 function __construct($storage, &$logger, $batcher = false, $backupStart = false) {
80
81 /**
82 * WP Global Database variable
83 */
84 global $wpdb;
85 $this->wpdb = &$wpdb;
86
87 /**
88 * Logger of BMI core
89 */
90 $this->logger = &$logger;
91
92 /**
93 * Storage directory
94 */
95 // $this->storage = trailingslashit(__DIR__) . 'data';
96 $this->storage = $storage;
97
98 /**
99 * Percentage escape to replace
100 * This way we know what the randomized string is
101 */
102 $this->percentage = trim($this->wpdb->prepare('%s', '%'), "'");
103
104 /**
105 * Max rows to pass each query
106 */
107 $this->max_rows = BMI_DB_MAX_ROWS_PER_QUERY;
108
109 /**
110 * Max size in bytes of single query export/import
111 */
112 $this->max_query_size = 1 * 1024 * 1024;
113
114 $this->table_prefix = time();
115 if ($backupStart && $backupStart !== false && is_numeric($backupStart)) {
116 $this->table_prefix = $backupStart;
117 }
118 $this->init_start = microtime(true);
119 if ($batcher === false || $batcher === 0) {
120 $this->logger->log("Memory usage after initialization: " . number_format(memory_get_usage() / 1024 / 1024, 2) . " MB", 'INFO');
121 }
122
123 }
124
125 /**
126 * export - Export initializer
127 *
128 * @return filename/filenames
129 */
130 public function export($batchingStep = false, $indexEnded = 0) {
131
132 // Table names
133 $this->get_table_names_and_sizes($batchingStep);
134 if ($batchingStep === false || $batchingStep === 0) {
135 $this->logger->log("Scan found $this->total_tables tables ($this->total_rows rows), estimated total size: $this->total_size MB.", 'INFO');
136 $this->logger->log("Memory usage after getting table names: " . number_format(memory_get_usage() / 1024 / 1024, 2) . " MB ", 'INFO');
137 }
138
139 // Recipes
140 if ($batchingStep === false || $batchingStep === 0) {
141 $this->logger->log("Getting table recipes...", 'INFO');
142 $this->table_recipes();
143 $this->logger->log("Table recipes have been exported.", 'INFO');
144 $this->logger->log("Memory usage after loading recipes: " . number_format(memory_get_usage() / 1024 / 1024, 2) . " MB ", 'INFO');
145 }
146
147 // Save Recipes
148 if ($batchingStep === false || $batchingStep === 0) {
149 $this->logger->log("Saving recipes...", 'INFO');
150 $this->save_recipes();
151 $this->logger->log("Recipes saved.", 'INFO');
152 $this->logger->log("Memory usage after recipe off-load: " . number_format(memory_get_usage() / 1024 / 1024, 2) . " MB", 'INFO');
153 }
154
155 // Tables data
156 if ($batchingStep === false || $batchingStep === 0) {
157 $this->logger->log("Exporting table data...", 'INFO');
158 }
159 $finishedAt = $this->get_tables_data($batchingStep, $indexEnded);
160 if ($batchingStep === false || $finishedAt['dumpCompleted'] === true) {
161 $this->logger->log("Table data exported.", 'INFO');
162 $this->logger->log("Memory usage after data export: " . number_format(memory_get_usage() / 1024 / 1024, 2) . " MB", 'INFO');
163 }
164
165 if ($batchingStep === false) {
166 $end = number_format(microtime(true) - $this->init_start, 4);
167 $this->logger->log("Entire process took: $end s", 'INFO');
168 }
169
170 return $finishedAt;
171
172 }
173
174 /**
175 * getPrefixesOfStagingSitesTables - Gets database prefixes of staging sites
176 *
177 * @return {array} of prefixes (can be empty)
178 */
179 private function getPrefixesOfStagingSitesTables() {
180
181 global $table_prefix;
182
183 require_once BMI_INCLUDES . '/staging/controller.php';
184 $staging = new Staging('..ajax..');
185 $stagingSites = $staging->getStagingSites(true);
186 $prefixes = [];
187
188 foreach ($stagingSites as $name => $data) {
189 if (!isset($data['db_prefix'])) continue;
190 if (!$data['db_prefix']) continue;
191 if (trim(strtolower($data['db_prefix'])) == '') continue;
192 if (trim(strtolower($data['db_prefix'])) == trim(strtolower($table_prefix))) continue;
193 if (isset($data['db_prefix'])) $prefixes[] = $data['db_prefix'];
194 }
195
196 return $prefixes;
197
198 }
199
200 /**
201 * checkIfTableIsPartOfStaging - Compares table prefix with staging site prefixes
202 *
203 * @return {bool} true/false
204 */
205 private function checkIfTableIsPartOfStaging(&$stagingSitePrefixes, $table) {
206
207 for ($i = 0; $i < sizeof($stagingSitePrefixes); ++$i) {
208 $prefix = $stagingSitePrefixes[$i];
209 if (substr($table, 0, strlen($prefix)) == $prefix) return true;
210 }
211
212 return false;
213
214 }
215
216 /**
217 * get_table_names_and_sizes - Gets table names and sizes
218 *
219 * @return {array} associative array table_name => [size => its size in MB, rows => rows count]
220 */
221 private function get_table_names_and_sizes($batchingStep) {
222
223 $tables = $this->wpdb->get_results('SHOW TABLES');
224 $shouldExcludeTables = Dashboard\bmi_get_config('BACKUP:DATABASE:EXCLUDE');
225 $stagingPrefixes = $this->getPrefixesOfStagingSitesTables();
226
227 $excludedTables = [];
228 $excludedTables = Dashboard\bmi_get_config('BACKUP:DATABASE:EXCLUDE:LIST');
229 if (!is_array($excludedTables) || empty($excludedTables)) $excludedTables = [];
230
231 foreach ($tables as $table_index => $table_object) {
232 foreach ($table_object as $database_name => $table_name) {
233
234 if (in_array($table_name, $excludedTables) && $shouldExcludeTables == 'true') {
235 $str = __('Excluding %s table from backup (due to exclusion rules).', 'backup-backup');
236 $str = str_replace('%s', $table_name, $str);
237 if ($batchingStep === false || intval($batchingStep) === 0) $this->logger->log($str, 'INFO');
238 continue;
239 }
240
241 if ($this->checkIfTableIsPartOfStaging($stagingPrefixes, $table_name)) {
242 $str = __('Excluding %s table from backup (as it is part of staging site).', 'backup-backup');
243 $str = str_replace('%s', $table_name, $str);
244 if ($batchingStep === false || intval($batchingStep) === 0) $this->logger->log($str, 'INFO');
245 continue;
246 }
247
248 $query = "SELECT table_name AS `table`, round(((data_length + index_length) / 1024 / 1024), 2) AS `size`, ";
249 $query .= "(SELECT COUNT(*) FROM `$table_name`) AS `rows`";
250 $query .= "FROM information_schema.TABLES ";
251 $query .= "WHERE table_schema = %s AND table_name = %s";
252 $results = $this->wpdb->get_results($this->wpdb->prepare($query, DB_NAME, $table_name));
253
254 if (!is_object($results[0])) {
255 if ($batchingStep === false || intval($batchingStep) === 0) {
256 $this->logger->log("Could not get info about: $table_name (#01)", 'WARN');
257 }
258 continue;
259 }
260
261 $table_name_returned = trim($results[0]->table);
262 if ($table_name != $table_name_returned || strlen(trim($table_name)) <= 0) {
263 if ($batchingStep === false || intval($batchingStep) === 0) {
264 $this->logger->log("Could not get info about: $table_name (#02)", 'WARN');
265 }
266 continue;
267 }
268
269 $this->tables_by_size[$table_name_returned] = array(
270 'size' => floatval($results[0]->size),
271 'rows' => intval($results[0]->rows)
272 );
273
274 $this->total_size += floatval($results[0]->size);
275 $this->total_rows += intval($results[0]->rows);
276 $this->total_tables++;
277
278 }
279 }
280
281 return $this->tables_by_size;
282
283 }
284
285 /**
286 * table_recipes - Gets CREATION recipe of each table
287 *
288 * @return {array} - Creation recipes for each table_name => recipe
289 */
290 private function table_recipes() {
291
292 foreach ($this->tables_by_size as $table_name => $table_object) {
293
294 $query = "SHOW CREATE TABLE $table_name";
295 $result = $this->wpdb->get_results($query);
296 foreach ($result as $index => $result_object) {
297 foreach ($result_object as $column_name => $column_value) {
298
299 if ($column_value == $table_name) continue;
300 else {
301
302 $column_value = str_replace("`" . $table_name . "`", "`" . $this->table_prefix . '_' . $table_name . "`", $column_value);
303
304 $recipe = 'CREATE TABLE IF NOT EXISTS ';
305 $recipe .= substr($column_value, 13);
306 $recipe = str_replace("\n ", "", $recipe);
307 $recipe = str_replace("\n", "", $recipe);
308
309 $this->recipes[$table_name] = $recipe;
310
311 }
312
313 }
314 }
315
316 }
317
318 return $this->recipes;
319
320 }
321
322 /**
323 * save_recipes - Save recipes and off-load the memory
324 *
325 * @return {void}
326 */
327 private function save_recipes() {
328
329 $time_prefix = $this->table_prefix;
330 foreach ($this->recipes as $table_name => $table_recipe) {
331
332 $this->total_queries += 3;
333 $recipe = "/* CUSTOM VARS START */\n";
334 $recipe .= "/* REAL_TABLE_NAME: `$table_name`; */\n";
335 $recipe .= "/* PRE_TABLE_NAME: `$time_prefix" . "_" . "$table_name`; */\n";
336 $recipe .= "/* CUSTOM VARS END */\n\n";
337
338 $recipe .= $table_recipe . ";\n";
339
340 $this->total_rows++;
341 $location = $this->file_name($table_name);
342 $file = fopen($location, 'w');
343 fwrite($file, $recipe);
344
345 fclose($file);
346 unset($file);
347
348 $this->files[] = $location;
349 unset($location);
350
351 }
352
353 unset($this->recipes);
354
355 }
356
357 private function getArraySize(&$a, $baseSize = 0) {
358
359 $maxSize = $this->max_query_size;
360 $totalSize = 0 + $baseSize;
361 $i = 0;
362 $reachedLimit = false;
363
364 foreach ($a as $k => $v) {
365 if (is_object($v) || is_array($v)) {
366 $subSize = $this->getArraySize($v);
367 $size = $subSize['size'];
368 if (($totalSize + $size) > $maxSize && $totalSize != 0) {
369 $reachedLimit = true;
370 break;
371 } else {
372 $totalSize += $size;
373 $i++;
374 }
375 } else if (strval($v)) {
376 $totalSize += strlen($v);
377 }
378 }
379
380 return [
381 'size' => $totalSize,
382 'index' => $i,
383 'limit' => $reachedLimit
384 ];
385
386 }
387
388 /**
389 * get_tables_data - Table data getter
390 *
391 * @return {int} Total rows count
392 */
393 private function get_tables_data($batchingStep = false, $indexEnded = 0) {
394
395 $finishedAt = 0;
396 $currentTableIndex = 0;
397 $dumpCompleted = true;
398
399 foreach ($this->tables_by_size as $table_name => $table_object) {
400
401 $emptyTable = false;
402 $currentTableIndex = $currentTableIndex + 1;
403
404 if ($batchingStep !== false) {
405 if (intval($currentTableIndex - 1) !== intval($batchingStep)) {
406 continue;
407 } else {
408 $dumpCompleted = false;
409 }
410 }
411
412 $start_time = microtime(true);
413 if ($batchingStep === false || intval($indexEnded) === 0) {
414 $this->logger->log("Getting data of table: " . $table_name . " (" . $currentTableIndex . "/" . $this->total_tables . ", " . number_format($table_object['size'], 2) . " MB)", 'STEP');
415 }
416 $rows = intval($table_object['rows']);
417
418 $this->wpdb->query("SET foreign_key_checks = 0;");
419
420 $currentBufferSize = 0;
421 $bufferResult = [];
422
423 $i = 0;
424 if ($batchingStep !== false) $i = $indexEnded;
425
426 if (intval($table_object['rows']) > 0) {
427 for (;$i < $rows;) {
428
429 $query = $this->wpdb->prepare("SELECT * FROM `$table_name` LIMIT %d, $this->max_rows", $i);
430 $result = $this->wpdb->get_results($query);
431
432 $valuesSize = $this->getArraySize($result, $currentBufferSize);
433 $rowsAmount = sizeof($result);
434 $valuesBytesSize = $valuesSize['size'] - $currentBufferSize;
435 $valuesMaxRow = $valuesSize['index'];
436 $valuesLimit = $valuesSize['limit'];
437
438 if ($valuesMaxRow < $rowsAmount && $valuesMaxRow != 0) $result = array_slice($result, 0, $valuesMaxRow);
439
440 $i += $valuesMaxRow;
441 $currentBufferSize += $valuesBytesSize;
442 $finishedAt = $i;
443
444 if ($valuesMaxRow != 0) $bufferResult = array_merge($bufferResult, $result);
445
446 if ($currentBufferSize >= $this->max_query_size || $i >= $rows || $valuesLimit == true) {
447
448 $currentBufferSize = 0;
449 $this->save_data($bufferResult, $table_name);
450 unset($bufferResult);
451 $bufferResult = [];
452
453 if ($batchingStep !== false) break;
454
455 }
456
457 unset($result);
458
459 }
460
461 $percentg = 100;
462 if (intval($table_object['rows']) !== 0 && is_numeric(intval($table_object['rows']))) {
463
464 $percentg = number_format(($i / intval($table_object['rows']) * 100), 2);
465
466 }
467
468 if ($i >= $rows && $batchingStep !== false) {
469
470 $batchingStep = $batchingStep + 1;
471 $finishedAt = 0;
472
473 $this->logger->log("Milestone of table " . $table_name . ": " . $i . "/" . $table_object['rows'] . " rows (" . $percentg . "%, " . number_format((microtime(true) - $start_time), 5) . "s)", 'INFO');
474 $this->logger->log("Table export for: " . $table_name . " finished", 'SUCCESS');
475
476 } else if ($batchingStep !== false) {
477
478 $this->logger->log("Milestone of table " . $table_name . ": " . $i . "/" . $table_object['rows'] . " rows (" . $percentg . "%, " . number_format((microtime(true) - $start_time), 5) . "s)", 'INFO');
479
480 }
481
482 $this->wpdb->query("SET foreign_key_checks = 1;");
483
484 if ($batchingStep === false) {
485
486 $this->logger->log("Table export for: " . $table_name . " finished (" . number_format((microtime(true) - $start_time), 5) . "s)", 'SUCCESS');
487
488 }
489
490 unset($start_time);
491
492 } else {
493
494 $this->logger->log("Table " . $table_name . " is empty, saving only recipe.", 'INFO');
495 $emptyTable = true;
496
497 if ($batchingStep !== false) {
498
499 $batchingStep = $batchingStep + 1;
500 $finishedAt = 0;
501
502 }
503
504 }
505
506 if ($batchingStep !== false && $emptyTable === false) break;
507
508 }
509
510 return [
511 'finishedQuery' => $finishedAt,
512 'batchingStep' => $batchingStep,
513 'dumpCompleted' => $dumpCompleted
514 ];
515
516 }
517
518 /**
519 * save_data - Saves table data/row as query
520 *
521 * @param {wpdb object} &$result Database query result
522 * @param {string} &$table_name Table name
523 * @return {void}
524 */
525 private function save_data(&$result, &$table_name) {
526
527 $columns_schema_added = false;
528 $file = fopen($this->file_name($table_name), 'a+');
529
530 $this->total_queries++;
531 $query = "INSERT INTO `" . $this->table_prefix . "_" . $table_name . "` ";
532
533 foreach ($result as $index => $result_object) {
534
535 $data_in_order = array();
536 $format_in_order = array();
537 $columns_in_order = array();
538
539 foreach ($result_object as $column_name => $value) {
540
541 $data_in_order[] = $value;
542 $columns_in_order[] = "`$column_name`";
543
544 if (is_numeric($value)) {
545
546 if (is_float($value + 0)) $format_in_order[] = '%f';
547 else $format_in_order[] = '%d';
548
549 } else if (gettype($value) == 'NULL') {
550
551 $format_in_order[] = '%null';
552
553 } else $format_in_order[] = '%s';
554
555 }
556
557 if ($columns_schema_added === false) {
558
559 $query .= "(" . implode(', ', $columns_in_order) . ") VALUES (";
560 $columns_schema_added = true;
561
562 } else {
563
564 $query = "),(";
565
566 }
567
568 $columns = sizeof($columns_in_order);
569 unset($columns_in_order);
570
571 // $query .= "/* VALUES START */\n";
572 for ($i = 0; $i < $columns; ++$i) {
573
574 if ($format_in_order[$i] == '%f') {
575
576 $query .= floatval($data_in_order[$i]);
577
578 } elseif ($format_in_order[$i] == '%d') {
579
580 $query .= intval($data_in_order[$i]);
581
582 } elseif ($format_in_order[$i] == '%null') {
583
584 $query .= 'NULL';
585
586 } else {
587
588 $query .= $this->wpdb->prepare("%s", $data_in_order[$i]);
589 $query = str_replace($this->percentage, '%', $query);
590
591 }
592
593 if ($i < ($columns - 1)) $query .= ",";
594
595 }
596
597 unset($data_in_order);
598 unset($format_in_order);
599 unset($columns_in_order);
600
601 fwrite($file, $query);
602
603 }
604
605 // fwrite($file, ");\n/* QUERY END */\n\n");
606 fwrite($file, ");\n");
607 fclose($file);
608 unset($file);
609
610 }
611
612 /**
613 * file_name - Replaces table name to file name friendly format
614 *
615 * @param {string} $table_name Table name
616 * @return {string} Friendly format for file
617 */
618 private function file_name($table_name) {
619
620 $friendly_name = preg_replace("/[^A-Za-z0-9_-]/", '', $table_name);
621 $friendly_name = trailingslashit($this->storage) . $friendly_name . '.sql';
622
623 return $friendly_name;
624
625 }
626
627 }
628