File: //wordpress/mu-plugins/photon-subsizes/photon-subsizes-cli.php
<?php
class Photon_Subsizes_Cli_Recover_Disk_Space {
private $is_dry_run = false;
private $is_verbose = false;
private $images_removed = 0;
private $bytes_recovered = 0;
function __construct( $cli_assoc_args ) {
$this->is_dry_run = WP_CLI\Utils\get_flag_value( $cli_assoc_args, 'dry-run', false /* default */ );
$this->is_verbose = WP_CLI\Utils\get_flag_value( $cli_assoc_args, 'verbose', false /* default */ );
}
function run() {
global $wpdb;
if ( ! photon_subsizes_site_has_standard_uploads_location() ) {
WP_CLI::error( 'Cannot run command. Site has a non-standard uploads location which is not supported by photon-subsizes.' );
}
$this->images_removed = 0;
$this->bytes_recovered = 0;
$upload_dir = wp_get_upload_dir();
if ( ! file_exists( $upload_dir['basedir'] ) ) {
WP_CLI::error( "Cannot find uploads directory: {$upload_dir['path']}" );
return;
}
if ( $this->is_dry_run ) {
WP_CLI::log( "Beginning dry run..." );
}
$disk_usage_before_removal = $this->get_site_disk_usage();
if ( false === $disk_usage_before_removal ) {
WP_CLI::warning( "Could not read disk usage info" );
}
$sql_is_photon_supported_mime_type = 'post_mime_type IN ( "image/jpeg", "image/png", "image/gif" )';
$count = $wpdb->get_var( $wpdb->prepare( "
SELECT COUNT(*)
FROM $wpdb->posts
WHERE
post_type = 'attachment' AND
$sql_is_photon_supported_mime_type
" ) );
$this->log_verbose( "Removing intermediate images for $count image attachments" );
$progress = WP_CLI\Utils\make_progress_bar( "Removing intermediate image files", $count );
$batch_size = 10;
$max_post_id = 0;
// Though this loop should break when it gets to the end of attachments,
// limit with max iterations as a safety against infinite looping
$max_iterations = ceil( $count / $batch_size );
for ( $i = 0; $i < $max_iterations; ++$i ) {
$attachments = $wpdb->get_results( $wpdb->prepare(
"
SELECT ID, file.meta_value as original, meta.meta_value AS meta, backup.meta_value as backup_sizes
FROM $wpdb->posts posts
INNER JOIN $wpdb->postmeta file ON
posts.ID=file.post_id AND
file.meta_key='_wp_attached_file'
INNER JOIN $wpdb->postmeta meta ON
posts.ID=meta.post_id AND
meta.meta_key='_wp_attachment_metadata'
LEFT OUTER JOIN $wpdb->postmeta backup ON
posts.ID=backup.post_id AND
backup.meta_key='_wp_attachment_backup_sizes'
WHERE
posts.ID > %d AND
posts.post_type = 'attachment' AND
$sql_is_photon_supported_mime_type
ORDER BY posts.ID
LIMIT %d
",
$max_post_id,
$batch_size
) );
if ( count( $attachments ) === 0 ) {
break;
}
foreach ( $attachments as $attachment ) {
$progress->tick();
$max_post_id = $attachment->ID;
$original_path = "{$upload_dir['basedir']}/{$attachment->original}";
if ( ! file_exists( $original_path ) ) {
WP_CLI::warning( "Skipping attachment. Original image '$original_path' does not exist." );
continue;
}
$attachment_meta = maybe_unserialize( $attachment->meta );
if ( ! isset( $attachment_meta['sizes'] ) ) {
WP_CLI::warning( "Skipping image. Cannot find sizes metadata for '$original_path'." );
continue;
}
$this->log_verbose( "Cleaning up intermediate images for $original_path" );
$base_path = dirname( $original_path );
$this->remove_subsizes( $base_path, $attachment_meta['sizes'] );
if ( null !== $attachment->backup_sizes ) {
$this->log_verbose( "Cleaning up backup intermediate images for $original_path" );
$backup_sizes = maybe_unserialize( $attachment->backup_sizes );
$this->remove_subsizes( $base_path, $backup_sizes );
}
}
}
$progress->finish();
$formatted_images_removed = number_format( $this->images_removed );
$formatted_size = size_format( $this->bytes_recovered, 1 );
$savings_percent = round( $this->bytes_recovered / $disk_usage_before_removal * 100, 1 );
WP_CLI::log(
"Removed {$formatted_images_removed} intermediate images for a total of {$formatted_size} " .
"which was {$savings_percent}% of the total disk space used by this site"
);
if ( ! $this->is_dry_run ) {
$this->log_verbose( "Bumping stats..." );
$stat_result = wp_safe_remote_get( "http://pixel.wp.com/g.gif?v=wpcom-no-pv&x_photon_subsizes/recovered_bytes={$this->bytes_recovered}" );
if ( is_wp_error( $stat_result ) ) {
WP_CLI::error( $stat_result, false /* do not exit */ );
}
$stat_result = wp_safe_remote_get( "http://pixel.wp.com/g.gif?v=wpcom-no-pv&x_photon_subsizes/removed_images={$this->images_removed}" );
if ( is_wp_error( $stat_result ) ) {
WP_CLI::error( $stat_result, false /* do not exit */ );
}
$this->log_verbose( "Done bumping stats." );
}
if ( $this->is_dry_run ) {
WP_CLI::log( "Dry run complete." );
}
}
private function remove_subsizes( $base_path, $sizes ) {
$removable_subsize_files = $this->get_removable_subsize_files( $base_path, $sizes );
foreach( $removable_subsize_files as $file_path => $size_in_bytes ) {
$this->images_removed++;
$this->bytes_recovered += $size_in_bytes;
$formatted_size = size_format( $size_in_bytes, 1 );
$this->log_verbose( "Deleting $formatted_size intermediate image $file_path" );
if ( ! $this->is_dry_run ) {
@unlink( $file_path );
}
}
}
private function get_removable_subsize_files( $base_path, $sizes ) {
// Collect files referenced by full image sizes so we can avoid removing
// full images that are also referenced by non-full size names
$full_size_files = array();
foreach ( $sizes as $name => $size ) {
if ( 1 === preg_match( "#^full(?:-|$)#", $name ) ) {
$this->log_verbose( "Found full image size: '$name' => '{$size['file']}'" );
$full_size_files[ $size['file'] ] = true;
}
}
$removable_files = array();
foreach ( $sizes as $name => $size ) {
if ( isset( $full_size_files[ $size['file'] ] ) ) {
$this->log_verbose( "Skipping subsize. File '{$size['file']}' is associated with a full image size." );
continue;
}
if ( 0 === preg_match( "#-\d+x\d+\.(?:png|jpg|jpeg|gif)$#i", $size['file'] ) ) {
$this->log_verbose( "Skipping subsize. File name does not match the intermediate image naming convention: {$size['file']}" );
continue;
}
$size_path = realpath( "{$base_path}/{$size['file']}" );
if ( false === $size_path ) {
$this->log_verbose( "Skipping subsize. File does not exist {$size['file']}." );
continue;
}
if ( 0 !== strpos( $size_path, $base_path ) ) {
WP_CLI::warning( "Skipping subsize. File '{$size_path}' is not under uploads dir." );
continue;
}
if ( isset( $removable_files[ $size_path ] ) ) {
$this->log_verbose( "Already found file '{$size_path}' referenced by another subsize" );
continue;
}
$stat = stat( $size_path );
if ( false === $stat ) {
WP_CLI::warning( "Skipping subsize. Could not stat() file {$size_path}." );
continue;
}
$this->log_verbose( "Found removable file {$size_path}" );
$removable_files[ $size_path ] = $stat['size'];
}
return $removable_files;
}
private function get_site_disk_usage() {
$site_info_json = file_get_contents( '/tmp/.at-site-info' );
if ( false === $site_info_json ) {
return false;
}
$site_info = json_decode( $site_info_json );
if ( null === $site_info ) {
return false;
}
if ( ! isset( $site_info->space_used ) || ! is_numeric( $site_info->space_used ) ) {
return false;
}
return $site_info->space_used;
}
private function log_verbose( $message ) {
if ( $this->is_verbose ) {
WP_CLI::log( $message );
}
}
}
/**
* Removes intermediate image files to recover disk space.
*
* ## Options
*
* [--verbose]
* : Enable verbose output.
*
* [--dry-run]
* : Simulate removal.
*/
function photon_subsizes_cli_recover_disk_space( $args = array(), $assoc_args = array() ) {
$runner = new Photon_Subsizes_Cli_Recover_Disk_Space( $assoc_args );
$runner->run();
}
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'atomic photon-subsizes-recover-space', 'photon_subsizes_cli_recover_disk_space' );
}