] * : Comma-separated parts: database,plugins,themes,uploads,mu-plugins. Default: all of them that exist. (WordPress itself and wp-config.php are not backed up or restored.) * * ## EXAMPLES * * wp wpcbr backup * wp wpcbr backup --targets=database,uploads * * @when after_wp_load */ public function backup($args, $assoc_args) { $targets = isset($assoc_args['targets']) ? explode(',', (string) $assoc_args['targets']) : WPCBR_Migration_Helper::get_default_selection(); $jobs = $this->jobs(); $job = $jobs->start('export', WPCBR_Migration_Helper::sanitize_selection($targets), array('split_size' => 0)); if (is_wp_error($job)) { WP_CLI::error($job->get_error_message()); } $job = $jobs->run_to_completion($job, $this->progress_logger()); if ('done' !== $job['phase']) { WP_CLI::error((string) $job['error']); } if (! empty($job['result']['skipped'])) { WP_CLI::warning(sprintf('%d file(s) could not be read and were left out (see skipped-files.txt in the ZIP).', (int) $job['result']['skipped'])); } WP_CLI::success('Backup created: ' . (new WPCBR_Backup_Service())->get_backup_dir() . '/' . $job['result']['file']); } /** * Restore a backup ZIP from a file on the server — no upload, so no size limit. * Nothing on the site changes until the very last step; the previous database is * kept as a restore point ("wp wpcbr rollback"). * * ## OPTIONS * * * : Path to the backup ZIP. * * [--targets=] * : Comma-separated parts to restore. Default: the default parts that the ZIP contains. * * [--replace-urls] * : Replace the source site's URLs with this site's (or --site-url / --home-url). * * [--site-url=] * : WordPress address to use after the restore. Default: the current one. * * [--home-url=] * : Site address to use after the restore. Default: the current one. * * [--yes] * : Do not ask for confirmation. * * ## EXAMPLES * * wp wpcbr import /home/user/wpcbr-backup-20261006.zip --replace-urls * * @when after_wp_load */ public function import($args, $assoc_args) { $restore = new WPCBR_Restore_Service(); $staged = $restore->stage_local_file((string) $args[0], false); if (is_wp_error($staged)) { WP_CLI::error($staged->get_error_message()); } // The staged copy is only for this run (also when it is declined or fails). register_shutdown_function( static function () use ($restore, $staged) { $restore->cleanup_staged_package($staged['token']); } ); $preview = $restore->get_staged_preview($staged['token']); $available = (array) ($preview['available_targets'] ?? array()); $targets = isset($assoc_args['targets']) ? explode(',', (string) $assoc_args['targets']) : array_values(array_intersect(WPCBR_Migration_Helper::get_default_selection(), $available)); $targets = WPCBR_Migration_Helper::sanitize_restore_selection($targets); WP_CLI::log(sprintf('Backup of %s, created %s.', $preview['manifest']['site_url'] ?? '?', $preview['manifest']['created_at_gmt'] ?? '?')); WP_CLI::confirm(sprintf('Restore %s into this site?', implode(', ', $targets)), $assoc_args); $key = strtolower(wp_generate_password(32, false, false)); WP_CLI::log('Recovery address (note it; it puts the original site back if anything goes wrong): ' . add_query_arg('wpcbr-recover', $key, home_url('/'))); WP_CLI::log('Or run "wp wpcbr recover --rollback" (also works with --skip-plugins --skip-themes).'); $jobs = $this->jobs(); $job = $jobs->start( 'import', $targets, array( 'token' => $staged['token'], 'selection' => $targets, 'replace_urls' => ! empty($assoc_args['replace-urls']), 'target_site_url' => (string) ($assoc_args['site-url'] ?? site_url()), 'target_home_url' => (string) ($assoc_args['home-url'] ?? home_url()), 'recovery_key' => $key, ) ); if (is_wp_error($job)) { WP_CLI::error($job->get_error_message()); } $job = $jobs->run_to_completion($job, $this->progress_logger()); if ('done' !== $job['phase']) { WP_CLI::error((string) $job['error']); } WP_CLI::success('Restored. The site before the restore is kept: "wp wpcbr recover --rollback" puts it back, "wp wpcbr recover --confirm" deletes it.'); } /** Progress lines for run_to_completion() (a closure, so it is not a command). */ private function progress_logger(): callable { return function (array $status) { if (! empty($status['label'])) { WP_CLI::log(sprintf('%s %s', $status['label'], $status['total'] > 0 ? '(' . $status['percent'] . '%)' : '')); } }; } private function jobs(): WPCBR_Job_Service { return new WPCBR_Job_Service(new WPCBR_Backup_Service(), new WPCBR_Restore_Service()); } /** * Roll the database back to the state captured before the last import. * * ## EXAMPLES * * wp wpcbr rollback * * @when after_wp_load */ public function rollback($args, $assoc_args) { if (WPCBR_Recovery::exists()) { WPCBR_Recovery::cli(array(), array('rollback' => true)); return; } $service = new WPCBR_Restore_Service(); if (empty($service->get_rollback_point()) && ! WPCBR_Recovery::legacy_pending()) { // A roll-back that stopped goes on (Codex 9回目). WP_CLI::error('No restore point is available to roll back to.'); } $result = $service->rollback_last_restore(); if (is_wp_error($result)) { WP_CLI::error($result->get_error_message()); } WP_CLI::success('Database rolled back to the pre-import state.'); } /** * Show whether a restore point (pre-import database copy) is available. * * ## EXAMPLES * * wp wpcbr status * * @when after_wp_load */ public function status($args, $assoc_args) { $service = new WPCBR_Restore_Service(); $point = $service->get_rollback_point(); if (WPCBR_Recovery::exists() || WPCBR_Recovery::is_v3_record($point)) { // A restore of this version (files and database together): "wp wpcbr recover" tells. WPCBR_Recovery::cli(array(), array()); return; } $unrecorded = $service->unrecorded_point_tables(); if (! empty($unrecorded)) { WP_CLI::warning($service->unrecorded_text($unrecorded)); } $others = $service->other_imported_tables(); if (! empty($others)) { WP_CLI::warning($service->other_imported_text($others)); } if (empty($point)) { WP_CLI::log('No restore point available.'); return; } $created = isset($point['created_gmt']) ? (string) $point['created_gmt'] : 'unknown'; $count = is_array($point['pairs'] ?? null) ? count($point['pairs']) : 0; WP_CLI::log(sprintf('Restore point available (created %s GMT, %d table(s) kept).', $created, $count)); WP_CLI::log('Run "wp wpcbr rollback" to revert, or "wp wpcbr cleanup --discard" to drop it.'); } /** * Remove leftover working tables left behind by an interrupted restore. * * ## OPTIONS * * [--discard] * : Also drop the kept rollback copy (you will no longer be able to roll back). * * [--imported=] * : Also clean up, as this site's, imported tables (wpcbrt_…) left behind that no other site * in this database names: a comma-separated list of the names "wp wpcbr status" shows, or "all". * "all" leaves the names KiraBackup could not check (status says why); name them to clean them up. * With nothing to clean up it says so and goes on (--discard still runs). * * ## EXAMPLES * * wp wpcbr cleanup * wp wpcbr cleanup --discard * wp wpcbr cleanup --imported=all * * @when after_wp_load */ public function cleanup($args, $assoc_args) { if (WPCBR_Recovery::exists()) { WP_CLI::error('A restore is in progress or waiting for your check; its working tables are still needed. Run "wp wpcbr recover" to see it.'); } $service = new WPCBR_Restore_Service(); // An import of this site under way (before its journal exists) still needs its tables // (QA 2026-10-09 参考): the clean-up waits for it. if ($service->import_running()) { WP_CLI::error('A restore of this site is running right now; its imported tables are still needed. Wait until it has finished, or cancel it on the KiraBackup screen first.'); } $service->cleanup_orphan_tables(); if (isset($assoc_args['imported'])) { $names = array_values(array_filter(array_map('trim', explode(',', (string) $assoc_args['imported'])))); if (empty($names)) { WP_CLI::error('Name the imported tables to clean up (comma-separated, as "wp wpcbr status" shows them), or "all".'); } $discarded = $service->discard_imported($names); if (is_wp_error($discarded) && 'wpcbr_nothing_offered' !== $discarded->get_error_code()) { WP_CLI::error($discarded->get_error_message()); } // "all" with nothing to clean up is not a failure: say so and go on, so that --discard // in the same command still runs (QA 2026-10-09 R2). WP_CLI::log(is_wp_error($discarded) ? $discarded->get_error_message() : 'Cleaned up the imported tables left behind, as this site\'s.'); } $discard = ! empty($assoc_args['discard']); if ($discard) { $discarded = $service->discard_rollback_point(); if (is_wp_error($discarded) && 'wpcbr_no_rollback' !== $discarded->get_error_code()) { WP_CLI::error($discarded->get_error_message()); } } $unrecorded = $service->unrecorded_point_tables(); if (! empty($unrecorded)) { WP_CLI::warning($service->unrecorded_text($unrecorded)); } $others = $service->other_imported_tables(); if (! empty($others)) { WP_CLI::warning($service->other_imported_text($others)); } WP_CLI::success($discard ? 'Removed leftover working tables and discarded the rollback copy.' : 'Removed leftover working tables (rollback copy preserved).'); } } WP_CLI::add_command('wpcbr', 'WPCBR_CLI');