] * : Encrypt the backup with this password (AES-256). * * ## EXAMPLES * * wp aisv backup * wp aisv backup --password=secret * * @param array $args Positional args. * @param array $assoc_args Associative args. * @return void */ public function backup( $args, $assoc_args ) { $pw = isset( $assoc_args['password'] ) ? (string) $assoc_args['password'] : ''; $id = Backup_Runner::start( $pw ); if ( is_wp_error( $id ) ) { \WP_CLI::error( $id->get_error_message() ); } \WP_CLI::log( sprintf( 'Backup started (job %d)…', (int) $id ) ); do { $res = Backup_Runner::step( (int) $id ); if ( ! empty( $res['error'] ) ) { \WP_CLI::error( isset( $res['message'] ) ? $res['message'] : 'Backup failed.' ); } if ( ! empty( $res['message'] ) ) { \WP_CLI::log( ' ' . $res['message'] ); } } while ( empty( $res['done'] ) ); \WP_CLI::success( 'Backup complete.' ); } /** * List stored backups. * * @subcommand backups * @param array $args Positional args. * @param array $assoc_args Associative args. * @return void */ public function backups( $args, $assoc_args ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $rows = $wpdb->get_results( "SELECT filename, size, destination, created_at FROM {$wpdb->prefix}aisiteark_backups ORDER BY created_at DESC, id DESC LIMIT 100", ARRAY_A ); if ( empty( $rows ) ) { \WP_CLI::log( 'No backups yet.' ); return; } \WP_CLI\Utils\format_items( 'table', $rows, array( 'filename', 'size', 'destination', 'created_at' ) ); } /** * Restore (or migrate) a backup by filename. * * ## OPTIONS * * * : The backup filename (see `wp aisv backups`). * * [--password=] * : Password for an encrypted backup. * * [--components=] * : Comma-separated subset to restore: database,uploads,themes,plugins,muplugins. Default: all. * * [--yes] * : Skip the confirmation prompt. * * [--force] * : Skip the pre-restore compatibility / integrity checks (restore anyway). * * [--replace-all] * : Empty the restored folders (uploads/themes/plugins/mu-plugins) first, so the site * becomes an exact copy of the backup instead of a merge. Files not in the backup are * deleted. WordPress core, the backups folder and AI-SiteArk itself are always kept. * * ## EXAMPLES * * wp aisv restore aisv-backup-20260706-1200-ab12.aisv --yes * wp aisv restore backup.aisv --components=database --yes * * @param array $args Positional args. * @param array $assoc_args Associative args. * @return void */ public function restore( $args, $assoc_args ) { $file = isset( $args[0] ) ? (string) $args[0] : ''; $path = $file ? Local_Store::path_for( $file ) : false; if ( ! $path ) { \WP_CLI::error( 'Backup file not found.' ); } \WP_CLI::confirm( 'This REPLACES the current site with the backup. Continue?', $assoc_args ); $pw = isset( $assoc_args['password'] ) ? (string) $assoc_args['password'] : ''; $components = array(); if ( ! empty( $assoc_args['components'] ) ) { $components = array_values( array_filter( array_map( 'trim', explode( ',', (string) $assoc_args['components'] ) ) ) ); } $force = ! empty( $assoc_args['force'] ); $replace_all = ! empty( $assoc_args['replace-all'] ); $started = Restore_Runner::start( $path, $pw, $components, $force, array(), null, $replace_all ); if ( is_wp_error( $started ) ) { if ( 'aisiteark_restore_incompat' === $started->get_error_code() ) { \WP_CLI::error( $started->get_error_message() . ' Re-run with --force to restore anyway.' ); } \WP_CLI::error( $started->get_error_message() ); } do { $res = Restore_Runner::step(); if ( ! empty( $res['error'] ) ) { \WP_CLI::error( isset( $res['message'] ) ? $res['message'] : 'Restore failed.' ); } if ( ! empty( $res['message'] ) ) { \WP_CLI::log( ' ' . $res['message'] ); } } while ( empty( $res['done'] ) ); \WP_CLI::success( 'Restore complete.' ); } }