├── LICENSE ├── README.md ├── composer.json ├── src ├── ServiceProvider.php ├── WpAdmin │ ├── BulkActions.php │ ├── Columns │ │ └── FileSize.php │ ├── Cron.php │ └── Meta │ │ └── Attachment.php └── resources │ └── views │ └── admin │ └── meta │ └── attachment.php └── wp-media-sortable-filesize.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Austin Passy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WordPress Media Sortable Filesize 2 | 3 | [![PHP from Packagist](https://img.shields.io/packagist/php-v/thefrosty/wp-media-sortable-filesize.svg)]() 4 | [![Latest Stable Version](https://img.shields.io/packagist/v/thefrosty/wp-media-sortable-filesize.svg)](https://packagist.org/packages/thefrosty/wp-media-sortable-filesize) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/thefrosty/wp-media-sortable-filesize.svg)](https://packagist.org/packages/thefrosty/wp-media-sortable-filesize) 6 | [![License](https://img.shields.io/packagist/l/thefrosty/wp-media-sortable-filesize.svg)](https://packagist.org/thefrosty/wp-media-sortable-filesize) 7 | 8 | Improve your Media Library functionality by introducing a new column that showcases the sizes of files. 9 | 10 | ## Package Installation (via Composer) 11 | 12 | `$ composer require thefrosty/wp-media-sortable-filesize:^1.0` 13 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thefrosty/wp-media-sortable-filesize", 3 | "description": "Improve your Media Library functionality by introducing a new column that showcases the sizes of files.", 4 | "type": "wordpress-plugin", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Austin Passy", 9 | "email": "thefrosty@users.noreply.github.com", 10 | "homepage": "https://austin.passy.co/" 11 | } 12 | ], 13 | "config": { 14 | "allow-plugins": { 15 | "composer/installers": true, 16 | "roots/wordpress-core-installer": true, 17 | "dealerdirect/phpcodesniffer-composer-installer": true 18 | }, 19 | "optimize-autoloader": true, 20 | "platform": { 21 | "php": "8.1" 22 | }, 23 | "preferred-install": "dist", 24 | "sort-packages": true 25 | }, 26 | "require": { 27 | "php": "^8.1", 28 | "composer/installers": "~2.0", 29 | "pimple/pimple": "^3.2", 30 | "psr/container": "^2.0", 31 | "symfony/http-foundation": "~6.4.14 || ^7.1.7", 32 | "thefrosty/wp-utilities": "^3.4" 33 | }, 34 | "require-dev": { 35 | "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", 36 | "roave/security-advisories": "dev-master", 37 | "roots/wordpress-no-content": "^6.6" 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "TheFrosty\\WpMediaSortableFilesize\\": "src" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | addPath(dirname(__DIR__) . '/src/resources/views/'); 29 | 30 | return $view; 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/WpAdmin/BulkActions.php: -------------------------------------------------------------------------------- 1 | addFilter('bulk_actions-upload', [$this, 'bulkActions']); 33 | $this->addFilter('handle_bulk_actions-upload', [$this, 'handleBulkActions'], 10, 3); 34 | } 35 | 36 | protected function bulkActions(array $actions): array 37 | { 38 | $actions[self::ACTION] = __('Generate filesize meta', 'media-library-filesize'); 39 | 40 | return $actions; 41 | } 42 | 43 | /** 44 | * Handle bulk action on upload.php (Media Library) 45 | * @param string $location 46 | * @param string $action 47 | * @param array $post_ids 48 | * @return string 49 | */ 50 | protected function handleBulkActions(string $location, string $action, array $post_ids): string 51 | { 52 | if ($action !== self::ACTION) { 53 | return $location; 54 | } 55 | 56 | foreach ($post_ids as $post_id) { 57 | $file = get_attached_file($post_id); 58 | if (!$file) { 59 | continue; 60 | } 61 | update_post_meta($post_id, FileSize::META_KEY, wp_filesize($file)); 62 | } 63 | 64 | // Schedule a cron event to update the cache count. 65 | Cron::scheduleSingleEventCount(); 66 | 67 | return $location; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/WpAdmin/Columns/FileSize.php: -------------------------------------------------------------------------------- 1 | addFilter('media_row_actions', [$this, 'mediaRowActions'], 10, 2); 87 | $this->addFilter('manage_media_columns', [$this, 'manageMediaColumns']); 88 | $this->addFilter('manage_upload_sortable_columns', [$this, 'manageUploadSortableColumns']); 89 | $this->addAction('added_post_meta', [$this, 'addFilesizeMetadata'], 10, 4); 90 | $this->addAction('manage_media_custom_column', [$this, 'manageMediaCustomColumn'], 10, 2); 91 | $this->addAction('load-upload.php', [$this, 'loadUploadPhp']); 92 | } 93 | 94 | /** 95 | * Add a new refresh filesize to the media row actions. 96 | * This allows users to refresh the custom post meta value if the file was changed externally or by a 3rd party. 97 | * @param array $actions 98 | * @param WP_Post $post 99 | * @return array 100 | */ 101 | protected function mediaRowActions(array $actions, WP_Post $post): array 102 | { 103 | $actions['refresh_filesize'] = sprintf( 104 | '%2$s', 105 | esc_url( 106 | wp_nonce_url( 107 | add_query_arg(['action' => self::NONCE_ACTION, 'attachment_id' => $post->ID]), 108 | self::NONCE_ACTION, 109 | self::NONCE_NAME 110 | ) 111 | ), 112 | esc_html__('Refresh filesize', 'media-sortable-filesize') 113 | );; 114 | 115 | return $actions; 116 | } 117 | 118 | /** 119 | * Add our column to the media columns array. 120 | * @param array $columns 121 | * @return array 122 | */ 123 | protected function manageMediaColumns(array $columns): array 124 | { 125 | $columns[self::META_KEY] = __('File Size', 'media-library-filesize'); 126 | 127 | return $columns; 128 | } 129 | 130 | /** 131 | * Add our column to the media columns sortable array. 132 | * @param array $columns 133 | * @return array 134 | */ 135 | protected function manageUploadSortableColumns(array $columns): array 136 | { 137 | $columns[self::META_KEY] = self::META_KEY; 138 | 139 | return $columns; 140 | } 141 | 142 | /** 143 | * Ensure file size meta gets added to new uploads. 144 | * @param int $meta_id The meta ID after successful update. 145 | * @param int $attachment_id ID of the object metadata is for. 146 | * @param string $meta_key Metadata key. 147 | * @param mixed $_meta_value Metadata value. 148 | */ 149 | protected function addFilesizeMetadata( 150 | int $meta_id, 151 | int $attachment_id, 152 | string $meta_key, 153 | mixed $_meta_value 154 | ): void { 155 | if ($meta_key !== '_wp_attachment_metadata') { 156 | return; 157 | } 158 | 159 | $file = get_attached_file($attachment_id); 160 | if (!$file) { 161 | return; 162 | } 163 | update_post_meta($attachment_id, self::META_KEY, wp_filesize($file)); 164 | } 165 | 166 | /** 167 | * Display our custom column data. 168 | * @param string $column_name 169 | * @param int $attachment_id 170 | */ 171 | protected function manageMediaCustomColumn(string $column_name, int $attachment_id): void 172 | { 173 | if ($column_name === self::META_KEY) { 174 | // First, try to get our attachment custom key value. 175 | $filesize = get_post_meta($attachment_id, self::META_KEY, true); 176 | $has_meta = is_numeric($filesize); 177 | 178 | 179 | if ($has_meta) { 180 | echo esc_html($this->sizeFormat($filesize)); 181 | $this->getIntermediateFilesizeHtml($attachment_id); 182 | return; 183 | } 184 | 185 | $warning = sprintf( 186 | ' ', 187 | esc_attr__('Missing attachment meta key, reading from meta or file.', 'media-sortable-filesize') 188 | ); 189 | 190 | // Second, try to get the attachment metadata filesize. 191 | self::getFileSize($attachment_id, $warning); 192 | } 193 | } 194 | 195 | /** 196 | * Additional class hooks that should only trigger on the current page "upload.php". 197 | */ 198 | protected function loadUploadPhp(): void 199 | { 200 | $this->addAction('restrict_manage_posts', [$this, 'restrictManagePosts']); 201 | $this->addAction('admin_print_styles', [$this, 'adminPrintStyles']); 202 | $this->addAction('pre_get_posts', [$this, 'preGetPosts']); 203 | $this->addAction('load-upload.php', function (): void { 204 | if ( 205 | (!$this->getRequest()->query->has('action') || !$this->getRequest()->query->has(self::NONCE_NAME)) || 206 | $this->getRequest()->query->get('action') !== self::NONCE_ACTION || 207 | !wp_verify_nonce($this->getRequest()->query->get(self::NONCE_NAME), self::NONCE_ACTION) 208 | ) { 209 | return; 210 | } 211 | 212 | // Single attachment cron event. 213 | if ( 214 | $this->getRequest()->query->has('attachment_id') && 215 | is_numeric($this->getRequest()->query->get('attachment_id')) 216 | ) { 217 | $attachment_id = $this->getRequest()->query->get('attachment_id'); 218 | $scheduled = wp_next_scheduled(Cron::HOOK_UPDATE_ID, [$attachment_id]); 219 | if (!$scheduled) { 220 | $schedule = wp_schedule_single_event(strtotime('now'), Cron::HOOK_UPDATE_ID, [(int)$attachment_id]); 221 | } 222 | $this->safeRedirect($schedule ?? $scheduled); 223 | } 224 | 225 | // All attachment's cron event. 226 | $scheduled = wp_next_scheduled(Cron::HOOK_UPDATE_META); 227 | if (!$scheduled) { 228 | $schedule = wp_schedule_single_event(strtotime('now'), Cron::HOOK_UPDATE_META); 229 | } 230 | $this->safeRedirect($schedule ?? $scheduled); 231 | }, 20); 232 | } 233 | 234 | /** 235 | * Add our button to the upload list view page. 236 | */ 237 | protected function restrictManagePosts(): void 238 | { 239 | $total = wp_count_posts('attachment')->inherit; 240 | $count = get_transient(Cron::TRANSIENT); 241 | if ($count === false) { 242 | Cron::scheduleSingleEventCount(); 243 | } 244 | printf( 245 | '%2$s', 246 | esc_url( 247 | wp_nonce_url( 248 | add_query_arg('action', self::NONCE_ACTION), 249 | self::NONCE_ACTION, 250 | self::NONCE_NAME 251 | ) 252 | ), 253 | sprintf( 254 | '%1$s %2$s', 255 | esc_html__('Index Media', 'media-sortable-filesize'), 256 | !is_numeric($count) ? '' : "(Total: $count/$total)" 257 | ), 258 | esc_attr__('Schedule the media filesize meta index cron to run now', 'media-sortable-filesize') 259 | ); 260 | } 261 | 262 | /** 263 | * Print our column style. 264 | */ 265 | protected function adminPrintStyles(): void 266 | { 267 | $column = esc_attr(self::META_KEY); 268 | echo << 270 | STYLE; 271 | } 272 | 273 | /** 274 | * Filter the attachments by filesize. 275 | * @param WP_Query $query 276 | */ 277 | protected function preGetPosts(WP_Query $query): void 278 | { 279 | global $pagenow; 280 | 281 | if ( 282 | !is_admin() || 283 | $pagenow !== 'upload.php' || 284 | !$query->is_main_query() || 285 | !$this->getRequest()->query->has('orderby') || 286 | $this->getRequest()->query->get('orderby') !== self::META_KEY 287 | ) { 288 | return; 289 | } 290 | 291 | $query->set('order', $this->getRequest()->query->get('order', 'desc')); 292 | $query->set('orderby', 'meta_value_num'); 293 | $query->set('meta_key', self::META_KEY); 294 | } 295 | 296 | /** 297 | * Calculates the total generated sizes of all intermediate image sizes. 298 | * @param int $attachment_id 299 | * @return int 300 | */ 301 | private function getIntermediateFilesize(int $attachment_id): int 302 | { 303 | $meta = wp_get_attachment_metadata($attachment_id); 304 | $size = 0; 305 | if (isset($meta['sizes'])) { 306 | foreach ($meta['sizes'] as $sizes) { 307 | $size += $sizes['filesize'] ?? 0; 308 | } 309 | } 310 | if (isset($meta['original_image']) && wp_get_original_image_path($attachment_id)) { 311 | $size += wp_filesize(wp_get_original_image_path($attachment_id)); 312 | } 313 | 314 | return absint($size); 315 | } 316 | 317 | /** 318 | * Get the intermediate image sizes HTML output. 319 | * @param int $attachment_id 320 | */ 321 | private function getIntermediateFilesizeHtml(int $attachment_id): void 322 | { 323 | $size = $this->getIntermediateFilesize($attachment_id); 324 | if ($size > 0) { 325 | printf( 326 | '
+ %1$s', 327 | esc_html($this->sizeFormat($size)), 328 | esc_attr__('Additional intermediate image sizes added together.', 'media-sortable-filesize') 329 | ); 330 | } 331 | } 332 | 333 | /** 334 | * Safe redirect exit helper. 335 | * @param mixed|null $scheduled 336 | * @return never 337 | */ 338 | private function safeRedirect(mixed $scheduled = null): never 339 | { 340 | wp_safe_redirect( 341 | remove_query_arg( 342 | ['action', 'attachment_id', 'nonce'], 343 | add_query_arg('scheduled', is_int($scheduled) ? $scheduled : $schedule ?? false) 344 | ) 345 | ); 346 | exit; 347 | } 348 | 349 | /** 350 | * Helper to get the size format with 2 decimals. 351 | * @param int|string $bytes 352 | * @return string 353 | */ 354 | private function sizeFormat(int | string $bytes): string 355 | { 356 | return size_format($bytes); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/WpAdmin/Cron.php: -------------------------------------------------------------------------------- 1 | addAction(self::HOOK_UPDATE_META, [$this, 'updateAllAttachmentsPostMeta']); 59 | $this->addAction(self::HOOK_UPDATE_COUNT, [$this, 'updateAttachmentCount']); 60 | $this->addAction(self::HOOK_UPDATE_ID, [$this, 'updateAttachmentIdMeta']); 61 | } 62 | 63 | /** 64 | * Run our update attachments post meta method. 65 | */ 66 | protected function updateAllAttachmentsPostMeta(): void 67 | { 68 | $seconds = 60; 69 | set_time_limit($seconds); 70 | $time = time(); 71 | $attachments = $this->getAllAttachments(); 72 | 73 | if (empty($attachments) || !count($attachments)) { 74 | return; 75 | } 76 | 77 | $error = new WP_Error(); 78 | foreach ($attachments as $attachment) { 79 | // Break out of the loop if whe have reached >= $seconds. 80 | if (time() - $time > $seconds) { 81 | break; 82 | } 83 | if (!get_post_meta($attachment, FileSize::META_KEY, true)) { 84 | $file = get_attached_file($attachment); 85 | // Make sure it's readable (on file-system). 86 | if (!is_string($file) || !is_readable($file)) { 87 | $error->add('not_found', 'File not found'); 88 | continue; 89 | } 90 | update_post_meta($attachment, FileSize::META_KEY, wp_filesize($file)); 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * Run our attachment count cache cron. 97 | */ 98 | protected function updateAttachmentCount(): void 99 | { 100 | $this->getAllAttachments(); 101 | } 102 | 103 | /** 104 | * Update the attachment filesize post meta. 105 | */ 106 | protected function updateAttachmentIdMeta(int|string $attachment_id): void 107 | { 108 | $previous = get_post_meta($attachment_id, FileSize::META_KEY, true); 109 | $file = get_attached_file($attachment_id); 110 | // Make sure it's readable (on file-system). 111 | if (!is_string($file) || !is_readable($file)) { 112 | return; 113 | } 114 | update_post_meta($attachment_id, FileSize::META_KEY, wp_filesize($file), $previous); 115 | } 116 | 117 | /** 118 | * Get all attachments query. 119 | * @return int[] array 120 | */ 121 | private function getAllAttachments(): array 122 | { 123 | $args = [ 124 | 'post_status' => 'inherit', 125 | 'meta_query' => [ 126 | [ 127 | 'key' => FileSize::META_KEY, 128 | 'compare' => 'NOT EXISTS', 129 | ], 130 | ], 131 | ]; 132 | $attachments = $this->wpQueryGetAllIdsCached('attachment', $args, MINUTE_IN_SECONDS); 133 | set_transient(self::TRANSIENT, count($attachments), WEEK_IN_SECONDS); 134 | 135 | return $attachments; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/WpAdmin/Meta/Attachment.php: -------------------------------------------------------------------------------- 1 | addAction('add_meta_boxes_attachment', [$this, 'addMetaBoxes']); 32 | } 33 | 34 | /** 35 | * Register our attachment meta box. 36 | */ 37 | protected function addMetaBoxes(): void 38 | { 39 | add_meta_box( 40 | esc_attr(self::class), 41 | __('Intermediate File Sizes', 'media-library-filesize'), 42 | function (WP_Post $post): void { 43 | $this->getView(ServiceProvider::WP_UTILITIES_VIEW)->render( 44 | 'admin/meta/attachment', 45 | [ 46 | 'metadata' => wp_get_attachment_metadata($post->ID), 47 | 'post' => $post, 48 | ] 49 | ); 50 | }, 51 | 'attachment', 52 | 'side' 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/resources/views/admin/meta/attachment.php: -------------------------------------------------------------------------------- 1 | ID); 8 | $sizes = get_intermediate_image_sizes(); 9 | 10 | foreach ($sizes as $size) { 11 | $image = wp_get_attachment_image_src($post->ID, $size); 12 | if (!is_array($image)) { 13 | continue; 14 | } 15 | 16 | $links[] = sprintf( 17 | '
  • %1$s: %3$s × %4$s (%5$s)
  • ', 18 | esc_html($size), 19 | esc_url($image[0]), 20 | esc_html($image[1]), 21 | esc_html($image[2]), 22 | esc_html(size_format($meta['sizes'][$size]['filesize'] ?? '')), 23 | ); 24 | } 25 | 26 | // This attachment has been "scaled" automatically by WordPress. 27 | if (isset($meta['original_image'])) { 28 | $original_image = wp_get_original_image_path($post->ID); 29 | if (!$original_image) { 30 | return; 31 | } 32 | $imagesize = wp_getimagesize($original_image); 33 | $links[] = sprintf( 34 | '
  • original_image: %2$s × %3$s (%4$s)
  • ', 35 | esc_url(wp_get_original_image_url($post->ID)), 36 | esc_html($imagesize[0]), 37 | esc_html($imagesize[1]), 38 | esc_html(size_format(wp_filesize($original_image))), 39 | ); 40 | } 41 | 42 | // Join the links in a string and return. 43 | printf( 44 | '
    ', 45 | implode('', $links) 46 | ); 47 | -------------------------------------------------------------------------------- /wp-media-sortable-filesize.php: -------------------------------------------------------------------------------- 1 | getContainer(); 36 | if (!$container instanceof Container) { 37 | throw new UnexpectedValueException('Unexpected object in Plugin container.'); 38 | } 39 | $container->register(new ServiceProvider()); 40 | 41 | $plugin 42 | ->add(new WpAdmin\Cron()) 43 | ->addOnHook(WpAdmin\BulkActions::class, 'init', admin_only: true) 44 | ->addOnHook(WpAdmin\Columns\FileSize::class, 'init', admin_only: true) 45 | ->addOnHook(WpAdmin\Meta\Attachment::class, 'admin_init', args: [$container]); 46 | 47 | if (is_admin()) { 48 | $plugin->add(new DisablePluginUpdateCheck()); 49 | } 50 | 51 | add_action('plugins_loaded', static function () use ($plugin): void { 52 | $plugin->initialize(); 53 | }); 54 | --------------------------------------------------------------------------------