| 1 |
<?php |
| 2 |
/** |
| 3 |
* StoreEngine full backup — WP-CLI commands. |
| 4 |
* |
| 5 |
* @version 1.0.0 |
| 6 |
*/ |
| 7 |
|
| 8 |
namespace StoreEngine\Backup; |
| 9 |
|
| 10 |
use WP_CLI; |
| 11 |
|
| 12 |
if ( ! defined( 'ABSPATH' ) ) { |
| 13 |
exit; |
| 14 |
} |
| 15 |
|
| 16 |
/** |
| 17 |
* Export / import a complete StoreEngine backup. |
| 18 |
*/ |
| 19 |
class Cli { |
| 20 |
|
| 21 |
/** |
| 22 |
* Export all StoreEngine data into a single .zip archive. |
| 23 |
* |
| 24 |
* ## OPTIONS |
| 25 |
* |
| 26 |
* [--file=<path>] |
| 27 |
* : Destination .zip path. Defaults to the private backups dir (path printed). |
| 28 |
* |
| 29 |
* [--groups=<list>] |
| 30 |
* : Comma list of optional groups to include: licensing,logs,users,files. |
| 31 |
* (Core store data + settings + catalog are always included.) |
| 32 |
* Default: licensing. |
| 33 |
* |
| 34 |
* ## EXAMPLES |
| 35 |
* |
| 36 |
* wp storeengine backup export --file=/tmp/se.zip --groups=licensing,users |
| 37 |
* |
| 38 |
* @when after_wp_load |
| 39 |
*/ |
| 40 |
public function export( $args, $assoc_args ) { |
| 41 |
$groups = array_filter( array_map( 'trim', explode( ',', (string) ( $assoc_args['groups'] ?? 'licensing' ) ) ) ); |
| 42 |
$opts = [ |
| 43 |
'licensing' => in_array( 'licensing', $groups, true ), |
| 44 |
'logs' => in_array( 'logs', $groups, true ), |
| 45 |
'users' => in_array( 'users', $groups, true ), |
| 46 |
'files' => in_array( 'files', $groups, true ), |
| 47 |
]; |
| 48 |
|
| 49 |
$progress = WP_CLI\Utils\make_progress_bar( 'Exporting', 100 ); |
| 50 |
$last = 0; |
| 51 |
$exporter = new Exporter( $opts, function ( $percent ) use ( $progress, &$last ) { |
| 52 |
$tick = (int) $percent - $last; |
| 53 |
if ( $tick > 0 ) { |
| 54 |
$progress->tick( $tick ); |
| 55 |
$last = (int) $percent; |
| 56 |
} |
| 57 |
} ); |
| 58 |
|
| 59 |
$path = $exporter->run(); |
| 60 |
$progress->finish(); |
| 61 |
|
| 62 |
if ( ! empty( $assoc_args['file'] ) ) { |
| 63 |
// phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename |
| 64 |
rename( $path, $assoc_args['file'] ); |
| 65 |
$path = $assoc_args['file']; |
| 66 |
} |
| 67 |
|
| 68 |
WP_CLI::success( sprintf( 'Backup written to %s (%s)', $path, size_format( (int) filesize( $path ) ) ) ); |
| 69 |
} |
| 70 |
|
| 71 |
/** |
| 72 |
* Restore a StoreEngine backup (REPLACES existing StoreEngine data). |
| 73 |
* |
| 74 |
* ## OPTIONS |
| 75 |
* |
| 76 |
* --file=<path> |
| 77 |
* : Path to the backup .zip. |
| 78 |
* |
| 79 |
* [--yes] |
| 80 |
* : Skip the confirmation prompt. |
| 81 |
* |
| 82 |
* [--no-safety] |
| 83 |
* : Skip the automatic pre-restore safety backup (not recommended). |
| 84 |
* |
| 85 |
* ## EXAMPLES |
| 86 |
* |
| 87 |
* wp storeengine backup import --file=/tmp/se.zip --yes |
| 88 |
* |
| 89 |
* @when after_wp_load |
| 90 |
*/ |
| 91 |
public function import( $args, $assoc_args ) { |
| 92 |
$file = (string) ( $assoc_args['file'] ?? '' ); |
| 93 |
if ( ! is_file( $file ) ) { |
| 94 |
WP_CLI::error( 'File not found: ' . $file ); |
| 95 |
} |
| 96 |
|
| 97 |
$preview = ( new Importer() )->inspect( $file ); |
| 98 |
$manifest = $preview['manifest']; |
| 99 |
WP_CLI::log( sprintf( 'Backup from %s (%s), generated %s.', $manifest['site_url'] ?? '?', $manifest['plugin_version'] ?? '?', $manifest['generated_at'] ?? '?' ) ); |
| 100 |
WP_CLI::log( sprintf( '%d tables; %d will be skipped (addon not installed).', count( (array) ( $manifest['tables'] ?? [] ) ), count( $preview['missing_tables'] ) ) ); |
| 101 |
|
| 102 |
WP_CLI::confirm( 'This will REPLACE all StoreEngine data on this site. Continue?', $assoc_args ); |
| 103 |
|
| 104 |
$progress = WP_CLI\Utils\make_progress_bar( 'Restoring', 100 ); |
| 105 |
$last = 0; |
| 106 |
$importer = new Importer( function ( $percent ) use ( $progress, &$last ) { |
| 107 |
$tick = (int) $percent - $last; |
| 108 |
if ( $tick > 0 ) { |
| 109 |
$progress->tick( $tick ); |
| 110 |
$last = (int) $percent; |
| 111 |
} |
| 112 |
} ); |
| 113 |
|
| 114 |
$result = $importer->run( $file, [ 'safety' => ! isset( $assoc_args['no-safety'] ) ] ); |
| 115 |
$progress->finish(); |
| 116 |
|
| 117 |
WP_CLI::log( sprintf( 'Restored %d tables, skipped %d.', count( $result['restored'] ), count( $result['skipped'] ) ) ); |
| 118 |
if ( ! empty( $result['safety_backup'] ) ) { |
| 119 |
WP_CLI::log( 'Pre-restore safety backup: ' . $result['safety_backup'] ); |
| 120 |
} |
| 121 |
WP_CLI::success( 'Restore complete.' ); |
| 122 |
} |
| 123 |
|
| 124 |
/** |
| 125 |
* Compare a backup's manifest row counts against the current database. |
| 126 |
* |
| 127 |
* ## OPTIONS |
| 128 |
* |
| 129 |
* --file=<path> |
| 130 |
* : Path to the backup .zip. |
| 131 |
* |
| 132 |
* @when after_wp_load |
| 133 |
*/ |
| 134 |
public function verify( $args, $assoc_args ) { |
| 135 |
global $wpdb; |
| 136 |
$file = (string) ( $assoc_args['file'] ?? '' ); |
| 137 |
if ( ! is_file( $file ) ) { |
| 138 |
WP_CLI::error( 'File not found: ' . $file ); |
| 139 |
} |
| 140 |
$manifest = ( new Importer() )->inspect( $file )['manifest']; |
| 141 |
$rows = []; |
| 142 |
foreach ( (array) ( $manifest['tables'] ?? [] ) as $t ) { |
| 143 |
$local = $wpdb->prefix . 'storeengine_' . $t['name']; |
| 144 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 145 |
$live = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$local}`" ); |
| 146 |
$rows[] = [ |
| 147 |
'table' => $t['name'], |
| 148 |
'in_backup' => (int) $t['row_count'], |
| 149 |
'in_db' => $live, |
| 150 |
'match' => ( (int) $t['row_count'] === $live ) ? 'yes' : 'NO', |
| 151 |
]; |
| 152 |
} |
| 153 |
WP_CLI\Utils\format_items( 'table', $rows, [ 'table', 'in_backup', 'in_db', 'match' ] ); |
| 154 |
} |
| 155 |
} |
| 156 |
|
| 157 |
// End of file cli.php. |
| 158 |
|