diff --git a/classes/Bulk/Bulk.php b/classes/Bulk/Bulk.php index 4e7eb4dd1..82d1cab7e 100644 --- a/classes/Bulk/Bulk.php +++ b/classes/Bulk/Bulk.php @@ -219,6 +219,61 @@ public function run_optimize( string $context, int $optimization_level ) { ]; } + /** + * Runs the bulk restore for a given context. + * + * Restores all optimized media to their original state synchronously. + * Does not use the Action Scheduler since restore is a local file + * operation that does not consume API quota. + * + * @since 2.3 + * + * @param string $context Current context (WP/Custom folders). + * + * @return array { + * @type bool $success Whether the operation was successful. + * @type string $message Status message. + * @type int $restored Number of media successfully restored. + * @type int $errors Number of media that failed to restore. + * @type int $total Total number of media processed. + * } + */ + public function run_restore( string $context ) { + $media_ids = $this->get_bulk_instance( $context )->get_optimized_media_ids(); + + if ( empty( $media_ids ) ) { + return [ + 'success' => false, + 'message' => 'no-images', + 'restored' => 0, + 'errors' => 0, + 'total' => 0, + ]; + } + + $restored = 0; + $errors = 0; + + foreach ( $media_ids as $media_id ) { + $process = imagify_get_optimization_process( $media_id, $context ); + $result = $process->restore(); + + if ( is_wp_error( $result ) ) { + ++$errors; + } else { + ++$restored; + } + } + + return [ + 'success' => true, + 'message' => 'success', + 'restored' => $restored, + 'errors' => $errors, + 'total' => count( $media_ids ), + ]; + } + /** * Runs the next-gen generation * diff --git a/classes/Bulk/BulkInterface.php b/classes/Bulk/BulkInterface.php index b599f6b32..d2eeb66e2 100644 --- a/classes/Bulk/BulkInterface.php +++ b/classes/Bulk/BulkInterface.php @@ -17,6 +17,15 @@ interface BulkInterface { */ public function get_unoptimized_media_ids( $optimization_level ); + /** + * Get all optimized media ids that can be restored. + * + * @since 2.3 + * + * @return array A list of optimized media IDs with backup files available. + */ + public function get_optimized_media_ids(); + /** * Get ids of all optimized media without Next gen versions. * diff --git a/classes/Bulk/CustomFolders.php b/classes/Bulk/CustomFolders.php index 19081e7d9..97d9e320c 100644 --- a/classes/Bulk/CustomFolders.php +++ b/classes/Bulk/CustomFolders.php @@ -78,6 +78,60 @@ public function get_unoptimized_media_ids( $optimization_level ) { return $files; } + /** + * Get all optimized media ids that can be restored. + * + * @since 2.3 + * + * @return array A list of optimized media IDs with backup files available. + */ + public function get_optimized_media_ids() { + global $wpdb; + + $this->set_no_time_limit(); + + $files_table = Imagify_Files_DB::get_instance()->get_table_name(); + $folders_table = Imagify_Folders_DB::get_instance()->get_table_name(); + + $files = $wpdb->get_results( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + " + SELECT fi.file_id, fi.path + FROM $files_table as fi + INNER JOIN $folders_table AS fo + ON ( fi.folder_id = fo.folder_id ) + WHERE + fi.status = 'success' + ORDER BY fi.file_id DESC" + ); + + $wpdb->flush(); + + if ( ! $files ) { + return []; + } + + $data = []; + + foreach ( $files as $file ) { + $file_id = absint( $file->file_id ); + + if ( empty( $file->path ) ) { + continue; + } + + $file_path = Imagify_Files_Scan::remove_placeholder( $file->path ); + $backup_path = Imagify_Custom_Folders::get_file_backup_path( $file_path ); + + if ( ! $this->filesystem->exists( $backup_path ) ) { + continue; + } + + $data[] = $file_id; + } + + return $data; + } + /** * Get ids of all optimized media without Next gen versions. * diff --git a/classes/Bulk/Noop.php b/classes/Bulk/Noop.php index 971967c65..3ceff9ac8 100644 --- a/classes/Bulk/Noop.php +++ b/classes/Bulk/Noop.php @@ -19,6 +19,17 @@ public function get_unoptimized_media_ids( $optimization_level ) { return []; } + /** + * Get all optimized media ids that can be restored. + * + * @since 2.3 + * + * @return array A list of optimized media IDs with backup files available. + */ + public function get_optimized_media_ids() { + return []; + } + /** * * Get ids of all optimized media without Next gen versions. * diff --git a/classes/Bulk/WP.php b/classes/Bulk/WP.php index afc1ffe07..81abdd3d3 100644 --- a/classes/Bulk/WP.php +++ b/classes/Bulk/WP.php @@ -171,6 +171,83 @@ public function get_unoptimized_media_ids( $optimization_level ) { return $data; } + /** + * Get all optimized media ids that can be restored. + * + * @since 2.3 + * + * @return array A list of optimized media IDs with backup files available. + */ + public function get_optimized_media_ids() { + global $wpdb; + + $this->set_no_time_limit(); + + $mime_types = Imagify_DB::get_mime_types(); + $statuses = Imagify_DB::get_post_statuses(); + $nodata_join = Imagify_DB::get_required_wp_metadata_join_clause(); + $nodata_where = Imagify_DB::get_required_wp_metadata_where_clause( + [ + 'prepared' => true, + ] + ); + + $ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + " + SELECT DISTINCT p.ID + FROM $wpdb->posts AS p + $nodata_join + INNER JOIN $wpdb->postmeta AS mt1 + ON ( p.ID = mt1.post_id AND mt1.meta_key = '_imagify_status' ) + WHERE + p.post_mime_type IN ( $mime_types ) + AND mt1.meta_value = 'success' + AND p.post_type = 'attachment' + AND p.post_status IN ( $statuses ) + $nodata_where + ORDER BY p.ID DESC" + ); + + $wpdb->flush(); + $ids = array_filter( array_map( 'absint', $ids ) ); + + if ( ! $ids ) { + return []; + } + + $metas = Imagify_DB::get_metas( + [ + // Get attachments filename. + 'filenames' => '_wp_attached_file', + ], + $ids + ); + + $data = []; + + foreach ( $ids as $id ) { + if ( empty( $metas['filenames'][ $id ] ) ) { + continue; + } + + $file_path = get_imagify_attached_file( $metas['filenames'][ $id ] ); + + if ( ! $file_path ) { + continue; + } + + $backup_path = get_imagify_attachment_backup_path( $file_path ); + + if ( ! $this->filesystem->exists( $backup_path ) ) { + continue; + } + + $data[] = $id; + } + + return $data; + } + /** * Get ids of all optimized media without Next gen versions. * diff --git a/classes/CLI/RestoreCommand.php b/classes/CLI/RestoreCommand.php new file mode 100644 index 000000000..0db85769f --- /dev/null +++ b/classes/CLI/RestoreCommand.php @@ -0,0 +1,151 @@ + + */ + private const CONTEXT_MAP = [ + 'library' => 'wp', + 'custom-folders' => 'custom-folders', + ]; + + /** + * Executes the command. + * + * @param array $arguments Positional argument. + * @param array $options Optional arguments. + */ + public function __invoke( $arguments, $options ) { + // Default to all contexts if none specified. + if ( empty( $arguments ) ) { + $arguments = array_keys( self::CONTEXT_MAP ); + } + + // Validate and map contexts. + $contexts = []; + + foreach ( $arguments as $arg ) { + if ( ! isset( self::CONTEXT_MAP[ $arg ] ) ) { + \WP_CLI::error( + sprintf( + 'Invalid context: "%s". Valid values are: %s', + $arg, + implode( ', ', array_keys( self::CONTEXT_MAP ) ) + ) + ); + } + + $contexts[] = self::CONTEXT_MAP[ $arg ]; + } + + $total_restored = 0; + $total_errors = 0; + + foreach ( $contexts as $index => $context ) { + $label = $arguments[ $index ]; + + \WP_CLI::log( sprintf( 'Restoring optimized media for: %s', $label ) ); + + $result = Bulk::get_instance()->run_restore( $context ); + + if ( ! $result['success'] ) { + \WP_CLI::warning( sprintf( 'No optimized media to restore for: %s', $label ) ); + continue; + } + + $total_restored += $result['restored']; + $total_errors += $result['errors']; + + \WP_CLI::log( + sprintf( + '%s: %d restored, %d errors out of %d total.', + ucfirst( $label ), + $result['restored'], + $result['errors'], + $result['total'] + ) + ); + } + + if ( 0 === $total_restored && 0 === $total_errors ) { + \WP_CLI::warning( 'No optimized media found to restore.' ); + return; + } + + if ( $total_errors > 0 ) { + \WP_CLI::warning( + sprintf( + 'Restore completed with errors: %d restored, %d failed.', + $total_restored, + $total_errors + ) + ); + return; + } + + \WP_CLI::success( + sprintf( + 'Restore completed: %d media restored successfully.', + $total_restored + ) + ); + } + + /** + * {@inheritdoc} + */ + protected function get_command_name(): string { + return 'restore'; + } + + /** + * {@inheritdoc} + */ + public function get_description(): string { + return 'Restore all optimized media to their original state'; + } + + /** + * {@inheritdoc} + */ + public function get_synopsis(): array { + return [ + [ + 'type' => 'positional', + 'name' => 'contexts', + 'description' => 'The context(s) to restore. Possible values are library and custom-folders. Defaults to all if omitted.', + 'optional' => true, + 'repeating' => true, + ], + ]; + } +} diff --git a/classes/Plugin.php b/classes/Plugin.php index 1f8f8cd6b..389eb36e1 100644 --- a/classes/Plugin.php +++ b/classes/Plugin.php @@ -5,7 +5,7 @@ use Imagify\Admin\AdminBar; use Imagify\Bulk\Bulk; -use Imagify\CLI\{BulkOptimizeCommand, GenerateMissingNextgenCommand}; +use Imagify\CLI\{BulkOptimizeCommand, GenerateMissingNextgenCommand, RestoreCommand}; use Imagify\Dependencies\League\Container\Container; use Imagify\Dependencies\League\Container\ServiceProvider\ServiceProviderInterface; use Imagify\EventManagement\{EventManager, SubscriberInterface}; @@ -133,6 +133,7 @@ class_alias( '\\Imagify\\Traits\\InstanceGetterTrait', '\\Imagify\\Traits\\FakeS imagify_add_command( new BulkOptimizeCommand() ); imagify_add_command( new GenerateMissingNextgenCommand() ); + imagify_add_command( new RestoreCommand() ); foreach ( $providers as $service_provider ) { $provider_instance = new $service_provider();