Is it possible to reorganize the WordPress uploads directory?

Unfortunately that’s a very sad fact about WordPress, it provides almost no customization for the uploads in the back-end (except the y/m organization stuff) .

What you can do, is to make your own custom class to generate the thumbnails (which are the most important part of performance impact, since every image generates a few to dozens of thumbnails) in a more organized way.

Step 1,2 and 3: Make a backup of all your uploads. What you are going to do can not be reversed unless you have a backup of your uploads folder!

Step 4 : Download and install the Thumbnail Cleaner plugin. This allows you to delete every generated thumbnail.

Step 5 : Create your own generation method. I’m having an example for you:

add_filter('wp_image_editors', 'my_wp_image_editors');
function my_wp_image_editors($editors) {
    array_unshift($editors, "my_WP_Image_Editor");
    return $editors;
}
// Include the existing classes first in order to extend them.
require_once ABSPATH . WPINC . "/class-wp-image-editor.php";
require_once ABSPATH . WPINC . "/class-wp-image-editor-gd.php";
// Now we extend the original image editor class
class my_WP_Image_Editor extends WP_Image_Editor_GD {
    public function generate_filename($suffix = null, $dest_path = null, $extension = null) {
        // $suffix will be appended to the destination filename, just before the extension
        if (!$suffix) {
            $suffix = $this->get_suffix();
        }
        $dir = pathinfo($this->file, PATHINFO_DIRNAME);
        $ext = pathinfo($this->file, PATHINFO_EXTENSION);
        $name = wp_basename($this->file, ".$ext");
        $new_ext = strtolower($extension ? $extension : $ext );
        if (!is_null($dest_path) && $_dest_path = realpath($dest_path)) {
            $dir = $_dest_path;
        }
        //we get the dimensions using explode, we could have used the properties of $this->file[height] but the suffix could have been provided
        $size_from_suffix = explode("x", $suffix);
        //we get the slug_name for this dimension
        $slug_name = $this->get_slug_by_size($size_from_suffix[0], $size_from_suffix[1]);
        return trailingslashit( $dir ) . "{$slug_name}/{$name}.{$new_ext}";
    }
    function multi_resize($sizes) {
        $metadata = array();
        $orig_size = $this->size;
        foreach ( $sizes as $size => $size_data ) {
            if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) {
                continue;
            }
            if ( ! isset( $size_data['width'] ) ) {
                $size_data['width'] = null;
            }
            if ( ! isset( $size_data['height'] ) ) {
                $size_data['height'] = null;
            }
            if ( ! isset( $size_data['crop'] ) ) {
                $size_data['crop'] = false;
            }
            $image = $this->_resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
            if ( ! is_wp_error( $image ) ) {
                $resized = $this->_save( $image );
                imagedestroy( $image );
                if ( ! is_wp_error( $resized ) && $resized ) {
                    unset( $resized['path'] );
                    $metadata[$size] = $resized;
                }
            }
            $this->size = $orig_size;
        }
        //we add the slug to the file path
        foreach ($metadata as $slug => $data) {
            $metadata[$slug]['file'] = $slug . "https://wordpress.stackexchange.com/" . $data['file'];
        }
        return $metadata;
    }
    // Our custom function to retrieve the proper slug by weight and height
    function get_slug_by_size($width, $height) {
        // Make thumbnails and other intermediate sizes.
        $_wp_additional_image_sizes = wp_get_additional_image_sizes();
        $image_sizes = array(); //all sizes the default ones and the custom ones in one array
        foreach (get_intermediate_image_sizes() as $s) {
            $image_sizes[$s] = array('width' => '', 'height' => '', 'crop' => false);
            if (isset($_wp_additional_image_sizes[$s]['width'])) {
                // For theme-added sizes
                $image_sizes[$s]['width'] = intval($_wp_additional_image_sizes[$s]['width']);
            } else {
                // For default sizes set in options
                $image_sizes[$s]['width'] = get_option("{$s}_size_w");
            }
            if (isset($_wp_additional_image_sizes[$s]['height'])) {
                // For theme-added sizes
                $image_sizes[$s]['height'] = intval($_wp_additional_image_sizes[$s]['height']);
            } else {
                // For default sizes set in options
                $image_sizes[$s]['height'] = get_option("{$s}_size_h");
            }
            if (isset($_wp_additional_image_sizes[$s]['crop'])) {
                // For theme-added sizes
                $image_sizes[$s]['crop'] = $_wp_additional_image_sizes[$s]['crop'];
            } else {
                // For default sizes set in options
                $image_sizes[$s]['crop'] = get_option("{$s}_crop");
            }
        }
        $slug_name = ""; //the slug's name
        if($width >= $height){
            foreach ($image_sizes as $slug => $data) { // we start checking
                if ($data['width'] == $width) {//we use only width because regardless of the height, the width is the one used for resizing in all cases with crop 1 or 0
                    $slug_name = $slug;
                }
                /*
                 * There could be custom added image sizes that have the same width as one of the defaults so we also use height here
                 * if there are several image sizes with the same width all of them will override the previous one leaving the last one, here we get also the last one
                 * since is looping the entire list, the height is used as a max value for non-hard cropped sizes
                 *  */
                  if ($data['width'] == $width && $data['height'] == $height) {
                      $slug_name = $slug;
                  }
        }
        } else {
            foreach ($image_sizes as $slug => $data) {
                if ($data['height'] == $height) {
                    $slug_name = $slug;
                }
                if ($data['height'] == $height && $data['width'] == $width ) {
                    $slug_name = $slug;
                }
            }
        }
        return $slug_name;
    }
}

This class is almost copied from the original class included in class-wp-image-editor-gd.php, with one difference : It will store the thumbnails in separate folders, all inside the uploads directory based on their size slug. So after you upload an image, you will end up with something like this:

/uploads/image.jpg
/uploads/thumbnail/image.jpg
/uploads/medium/image.jpg
/uploads/large/image.jpg
// And so on...

This will prevent having a million of images inside a single folder. You can edit the class the way you want, change the paths, and more. It was provided as an example to demonstrate how the images are generated and stored.

Step 6 : Use Regenerate Thumbnails Plugin to fill your uploads folder with newly generated thumbnails, in a fancy way. This will prevent a couple of thousands of thumbnails all being stored in a single folder. A working example can be found here. Right click and open a thumbnail in a new tab, and try changing the slug to see how it works.

I hope this gave you a deeper understanding about how to manipulate image generation in WordPress.

Leave a Comment