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 updatef001
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