├── 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 | []()
4 | [](https://packagist.org/packages/thefrosty/wp-media-sortable-filesize)
5 | [](https://packagist.org/packages/thefrosty/wp-media-sortable-filesize)
6 | [](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 | ' ',
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 | '