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