Skip to content

Instantly share code, notes, and snippets.

@austinginder
Last active February 9, 2025 12:18
Show Gist options
  • Save austinginder/9db3a634286f0adf6451a1ee3f8a4c2b to your computer and use it in GitHub Desktop.
Save austinginder/9db3a634286f0adf6451a1ee3f8a4c2b to your computer and use it in GitHub Desktop.
Offloading WordPress uploads to B2

NGINX rules for serving offloaded WordPress uploads to B2 bucket

  • Create a public B2 bucket and path to house your WordPress uploads.
  • Add the following NGINX rules to Kinsta and replace Bucket/folder with newly created B2 path. Also update f001 according to your B2 bucket settings.
  • Use rclone to move uploads from your WordPress uploads to B2.
  • Enjoy!
 # Serve local uploads from disk.
  location ^~ /wp-content/uploads/ {
    try_files $uri @b2stream;
  }

  # Stream response from Backblaze if the file is missing
  location @b2stream {
    internal;
    rewrite ^/wp-content/uploads/(.*)$ /$1 break;
    proxy_pass https://f001.backblazeb2.com/file/Bucket/folder/$1;
    proxy_redirect off;
    proxy_set_header Host f001.backblazeb2.com;
    proxy_buffering off; # Disable proxy buffering
  }

Example rclone setup:

rclone.config

[production]
type = sftp
key_file = /home/.ssh/key_file
host = xxx.xxx.xxx.xxx
user = username
port = 22
shell_type = unix
md5sum_command = md5sum
sha1sum_command = sha1sum

[offload]
type = b2
account = xxxxxxxxxx
key = xxxxxxxxxxxxxxxxxxxx
endpoint =

With that, the following command will selectively move certain files over to B2 bucket. This can be configured from any computer with rclone installed and scheduled for a very efficient offload.

rclone --config=rclone.conf move production:public/wp-content/uploads offload:Bucket/uploads/my-site/ \
--include "*.pdf" \
--include "*.jpg" \
--include "*.jpeg" \
--include "*.png" \
--include "*.gif" \
--include "*.mp3" \
--include "*.mov" \
--include "*.mp4" \
--include "*.webp" \
--include "*.avif" \
--include "*.svg" \
--include "*.apng" \
--include "*.ogg" \
--include "*.webm" \
--include "*.mkv" \
--include "*.avi"

Deleting an item in the WordPress media library won’t delete anything from the bucket. To track file delections create the following captaincore-offload-tracker.php must-use plugin.

<?php
namespace CaptainCore;
/**
 * Plugin Name: B2 Uploads Deletion Tracker
 * Description: Tracks WordPress attachment deletions by creating .deleted files.
 * Version: 1.0.0
 * Author: Austin Ginder
 */

 class B2DeletionTracker {

    /**
     * Constructor.
     */
    public function __construct() {
        add_action( 'delete_attachment', [ $this, 'handle_attachment_deletion' ] );
    }

    /**
     * Handles attachment deletion: creates a .deleted file for the main file and thumbnails, ensuring the directory exists.
     *
     * @param int $post_id The ID of the attachment being deleted.
     */
    public function handle_attachment_deletion( $post_id ) {
        // Get the attachment URL.
        $attachment_url = wp_get_attachment_url( $post_id );

        if ( ! $attachment_url ) {
            // Attachment URL not found, possibly already deleted or corrupted.
            return;
        }

        // Get the path to the file in the uploads directory.
        $upload_dir = wp_upload_dir();
        $file_path  = str_replace( $upload_dir['baseurl'], $upload_dir['basedir'], $attachment_url );

        // Create .deleted file for the main file.
        $this->create_deleted_file( $file_path );

        // Get attachment metadata to find thumbnails.
        $image_meta = wp_get_attachment_metadata( $post_id );

        if ( isset( $image_meta['sizes'] ) && is_array( $image_meta['sizes'] ) ) {
            foreach ( $image_meta['sizes'] as $size => $size_data ) {
                // Construct the thumbnail file path.
                $thumbnail_file_path = str_replace(
                    basename( $file_path ),
                    $size_data['file'],
                    $file_path
                );

                // Create .deleted file for the thumbnail.
                $this->create_deleted_file( $thumbnail_file_path );
            }
        }
    }

    /**
     * Creates a .deleted file for a given file path, ensuring the directory exists.
     *
     * @param string $file_path The path to the file.
     */
    private function create_deleted_file( $file_path ) {
        // Create the .deleted file path.
        $deleted_file_path = $file_path . '.deleted';

        // Ensure the directory exists.
        $deleted_file_dir = dirname( $deleted_file_path );
        if ( ! is_dir( $deleted_file_dir ) ) {
            $result = wp_mkdir_p( $deleted_file_dir ); // Use WordPress's recursive mkdir
            if ( ! $result ) {
                error_log(
                    'B2 Deletion Tracker: Failed to create directory: ' .
                    $deleted_file_dir
                );
                return; // Don't proceed if directory creation fails.
            }
        }

        // Create the .deleted file.
        if ( ! file_exists( $deleted_file_path ) ) {
            $result = touch( $deleted_file_path );
            if ( $result === false ) {
                error_log(
                    'B2 Deletion Tracker: Failed to create .deleted file: ' .
                    $deleted_file_path
                );
            }
        }
    }
}

new B2DeletionTracker();

The following bash script will loop through any found .deleted files and remove from WordPress and bucket storage.

deleted_files=$( rclone --config=rclone.conf --files-only --recursive --include "*.deleted" lsf production:public/wp-content/uploads )
for deleted_file in ${deleted_files[@]}; do
   rclone delete "production:public/wp-content/uploads/${deleted_file}.deleted"
   rclone delete "offload:Bucket/uploads/my-site/$deleted_file"
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment