Step-by-Step Guide to building a custom automated database backup engine block for Gutenberg using SolidJS high-performance reactive components
Designing the Automated Database Backup Engine Block
Building a robust, automated database backup solution integrated directly into a content management system (CMS) like WordPress requires a multi-faceted approach. For e-commerce platforms, where data integrity and rapid recovery are paramount, a custom Gutenberg block offers a powerful, user-friendly interface for managing these critical operations. This guide details the construction of such a block, leveraging SolidJS for its high-performance reactive components to ensure a seamless and efficient user experience within the WordPress admin area.
Our objective is to create a Gutenberg block that allows administrators to configure, trigger, and monitor database backups directly from the WordPress dashboard. This involves backend scripting for backup generation and storage, and a frontend component for user interaction. We’ll focus on a PHP-based backend for WordPress integration and SolidJS for the interactive frontend.
Backend: PHP Scripting for Database Backups
The core of our backup engine will be a PHP script that interfaces with the database, generates a dump, and stores it securely. For this example, we’ll assume a MySQL database. The script needs to be robust enough to handle potential errors and provide clear feedback.
First, let’s define the essential configuration parameters. These should ideally be stored in a secure, non-public location, perhaps within WordPress’s options API or a dedicated configuration file outside the webroot.
Database Configuration
These constants should be defined in a secure PHP file, e.g., wp-content/mu-plugins/backup-config.php. Using Must-Use plugins ensures they are always active.
wp-content/mu-plugins/backup-config.php
<?php
// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Database Credentials (Ideally fetched from wp-config.php or a more secure method)
define( 'DB_BACKUP_HOST', 'localhost' );
define( 'DB_BACKUP_USER', 'your_db_user' );
define( 'DB_BACKUP_PASS', 'your_db_password' );
define( 'DB_BACKUP_NAME', 'your_db_name' );
// Backup Storage Configuration
define( 'BACKUP_DIR', WP_CONTENT_DIR . '/db_backups/' ); // Ensure this directory is writable and NOT publicly accessible
define( 'MAX_BACKUPS_TO_KEEP', 7 ); // Number of recent backups to retain
define( 'BACKUP_FILENAME_FORMAT', 'db_backup_%Y-%m-%d_%H-%M-%S.sql.gz' ); // Timestamped filename with compression
?>
Backup Generation Script
This script will be responsible for creating the SQL dump and compressing it. We’ll use the `mysqldump` command-line utility, which is standard on most MySQL installations.
wp-content/mu-plugins/backup-engine.php
<?php
// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Include configuration
require_once __DIR__ . '/backup-config.php';
class Database_Backup_Engine {
/**
* Generates a compressed database backup.
*
* @return string|false Path to the backup file on success, false on failure.
*/
public function generate_backup() {
if ( ! defined( 'DB_BACKUP_HOST' ) || ! defined( 'DB_BACKUP_USER' ) || ! defined( 'DB_BACKUP_NAME' ) ) {
error_log( 'Database backup configuration is incomplete.' );
return false;
}
// Ensure backup directory exists and is writable
if ( ! is_dir( BACKUP_DIR ) ) {
if ( ! wp_mkdir_p( BACKUP_DIR ) ) {
error_log( 'Failed to create backup directory: ' . BACKUP_DIR );
return false;
}
}
if ( ! is_writable( BACKUP_DIR ) ) {
error_log( 'Backup directory is not writable: ' . BACKUP_DIR );
return false;
}
$filename = date( 'Y-m-d_H-i-s' ) . '_' . DB_BACKUP_NAME . '.sql';
$filepath = BACKUP_DIR . $filename;
$compressed_filepath = $filepath . '.gz';
// Construct the mysqldump command
// Ensure the path to mysqldump is correct for your server environment
$command = sprintf(
'mysqldump --host=%s --user=%s --password=%s %s > %s',
escapeshellarg( DB_BACKUP_HOST ),
escapeshellarg( DB_BACKUP_USER ),
escapeshellarg( DB_BACKUP_PASS ),
escapeshellarg( DB_BACKUP_NAME ),
escapeshellarg( $filepath )
);
// Execute the command
$output = null;
$return_var = null;
exec( $command, $output, $return_var );
if ( $return_var !== 0 ) {
error_log( "mysqldump failed with return code {$return_var}: " . implode( "\n", $output ) );
// Clean up partial dump file if it exists
if ( file_exists( $filepath ) ) {
unlink( $filepath );
}
return false;
}
// Compress the dump file using gzip
$compress_command = sprintf(
'gzip %s',
escapeshellarg( $filepath )
);
exec( $compress_command, $output, $return_var );
if ( $return_var !== 0 ) {
error_log( "gzip compression failed with return code {$return_var}: " . implode( "\n", $output ) );
// Clean up uncompressed file if compression failed
if ( file_exists( $filepath ) ) {
unlink( $filepath );
}
return false;
}
// Prune old backups
$this->prune_old_backups();
return $compressed_filepath;
}
/**
* Removes old backups, keeping only the specified number.
*/
private function prune_old_backups() {
$files = glob( BACKUP_DIR . '*.sql.gz' );
if ( $files === false ) {
error_log( 'Error listing backup files.' );
return;
}
// Sort files by modification time, oldest first
usort( $files, function( $a, $b ) {
return filemtime( $a ) - filemtime( $b );
} );
$files_to_delete = array_slice( $files, 0, count( $files ) - MAX_BACKUPS_TO_KEEP );
foreach ( $files_to_delete as $file ) {
if ( is_file( $file ) ) {
if ( ! unlink( $file ) ) {
error_log( 'Failed to delete old backup: ' . $file );
}
}
}
}
/**
* Retrieves a list of available backups.
*
* @return array Array of backup file details.
*/
public function list_backups() {
$files = glob( BACKUP_DIR . '*.sql.gz' );
if ( $files === false ) {
return [];
}
$backups = [];
foreach ( $files as $file ) {
if ( is_file( $file ) ) {
$backups[] = [
'name' => basename( $file ),
'size' => round( filesize( $file ) / 1024, 2 ) . ' KB', // Size in KB
'date' => date( 'Y-m-d H:i:s', filemtime( $file ) ),
'path' => $file, // For potential download functionality
];
}
}
// Sort by date, newest first
usort( $backups, function( $a, $b ) {
return strtotime( $b['date'] ) - strtotime( $a['date'] );
} );
return $backups;
}
/**
* Downloads a specific backup file.
*
* @param string $filename The name of the backup file to download.
* @return bool True on success, false on failure.
*/
public function download_backup( $filename ) {
$filepath = BACKUP_DIR . basename( $filename ); // Sanitize filename
if ( ! file_exists( $filepath ) || ! is_readable( $filepath ) ) {
error_log( "Backup file not found or not readable: {$filepath}" );
return false;
}
header( 'Content-Description: File Transfer' );
header( 'Content-Type: application/octet-stream' );
header( 'Content-Disposition: attachment; filename="' . basename( $filepath ) . '"' );
header( 'Expires: 0' );
header( 'Cache-Control: must-revalidate' );
header( 'Pragma: public' );
header( 'Content-Length: ' . filesize( $filepath ) );
readfile( $filepath );
return true;
}
/**
* Deletes a specific backup file.
*
* @param string $filename The name of the backup file to delete.
* @return bool True on success, false on failure.
*/
public function delete_backup( $filename ) {
$filepath = BACKUP_DIR . basename( $filename ); // Sanitize filename
if ( ! file_exists( $filepath ) ) {
error_log( "Backup file not found for deletion: {$filepath}" );
return false;
}
if ( ! unlink( $filepath ) ) {
error_log( "Failed to delete backup file: {$filepath}" );
return false;
}
return true;
}
}
// Instantiate the engine for potential direct use or API calls
$database_backup_engine = new Database_Backup_Engine();
WordPress REST API Integration
To allow our Gutenberg block to interact with the backend logic, we’ll expose endpoints via the WordPress REST API. This requires registering new routes and corresponding callback functions.
wp-content/mu-plugins/backup-api.php
<?php
// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Include the backup engine
require_once __DIR__ . '/backup-engine.php';
add_action( 'rest_api_init', function () {
global $database_backup_engine; // Access the globally instantiated engine
// Endpoint to trigger a new backup
register_rest_route( 'backup-engine/v1', '/generate', array(
'methods' => 'POST',
'callback' => function( WP_REST_Request $request ) use ( $database_backup_engine ) {
if ( ! current_user_can( 'manage_options' ) ) { // Ensure user has admin privileges
return new WP_Error( 'rest_forbidden', 'You do not have permission to perform this action.', array( 'status' => 403 ) );
}
$result = $database_backup_engine->generate_backup();
if ( $result ) {
return new WP_REST_Response( array( 'success' => true, 'message' => 'Backup generated successfully.', 'file' => basename( $result ) ), 200 );
} else {
return new WP_Error( 'backup_error', 'Failed to generate backup.', array( 'status' => 500 ) );
}
},
'permission_callback' => '__return_true', // Permissions handled inside callback
) );
// Endpoint to list existing backups
register_rest_route( 'backup-engine/v1', '/list', array(
'methods' => 'GET',
'callback' => function( WP_REST_Request $request ) use ( $database_backup_engine ) {
if ( ! current_user_can( 'manage_options' ) ) {
return new WP_Error( 'rest_forbidden', 'You do not have permission to perform this action.', array( 'status' => 403 ) );
}
$backups = $database_backup_engine->list_backups();
return new WP_REST_Response( array( 'success' => true, 'backups' => $backups ), 200 );
},
'permission_callback' => '__return_true',
) );
// Endpoint to download a backup
register_rest_route( 'backup-engine/v1', '/download/(?P<filename>[^/]+)', array(
'methods' => 'GET',
'callback' => function( WP_REST_Request $request ) use ( $database_backup_engine ) {
if ( ! current_user_can( 'manage_options' ) ) {
return new WP_Error( 'rest_forbidden', 'You do not have permission to perform this action.', array( 'status' => 403 ) );
}
$filename = $request->get_param( 'filename' );
// The download_backup method handles headers and output, so we just need to call it.
// It will exit the script if successful. If it returns false, we return an error.
if ( $database_backup_engine->download_backup( $filename ) ) {
// This part might not be reached if download_backup exits
return new WP_REST_Response( array( 'success' => true, 'message' => 'Download initiated.' ), 200 );
} else {
return new WP_Error( 'download_error', 'Failed to initiate download.', array( 'status' => 500 ) );
}
},
'permission_callback' => '__return_true',
) );
// Endpoint to delete a backup
register_rest_route( 'backup-engine/v1', '/delete/(?P<filename>[^/]+)', array(
'methods' => 'DELETE',
'callback' => function( WP_REST_Request $request ) use ( $database_backup_engine ) {
if ( ! current_user_can( 'manage_options' ) ) {
return new WP_Error( 'rest_forbidden', 'You do not have permission to perform this action.', array( 'status' => 403 ) );
}
$filename = $request->get_param( 'filename' );
if ( $database_backup_engine->delete_backup( $filename ) ) {
return new WP_REST_Response( array( 'success' => true, 'message' => 'Backup deleted successfully.' ), 200 );
} else {
return new WP_Error( 'delete_error', 'Failed to delete backup.', array( 'status' => 500 ) );
}
},
'permission_callback' => '__return_true',
) );
} );
Frontend: SolidJS Gutenberg Block Development
Now, we’ll build the Gutenberg block using SolidJS. This involves setting up a development environment, creating the block’s JavaScript/JSX files, and registering it with WordPress.
Development Environment Setup
A modern WordPress plugin development workflow typically uses Node.js and npm/yarn. We’ll use `@wordpress/scripts` for building our block assets.
Plugin Structure
Create a new plugin directory, e.g., wp-content/plugins/custom-backup-block/. Inside this directory, create the following structure:
custom-backup-block/ ├── custom-backup-block.php ├── build/ │ └── index.js ├── src/ │ ├── index.js │ ├── block.json │ └── components/ │ └── BackupManager.jsx └── package.json
custom-backup-block.php (Plugin Main File)
<?php
/**
* Plugin Name: Custom Backup Block
* Description: A Gutenberg block for managing database backups.
* Version: 1.0.0
* Author: Your Name
* Text Domain: custom-backup-block
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Registers the block using the metadata loaded from the `block.json` file.
* Behind the scenes, it registers also all assets so they can be enqueued
* through the block editor in the corresponding context.
*
* @see https://developer.wordpress.org/reference/functions/register_block_type/
*/
function custom_backup_block_init() {
register_block_type( __DIR__ . '/build', array(
'editor_script' => 'custom-backup-block-editor-script',
'editor_style' => 'custom-backup-block-editor-style',
'style' => 'custom-backup-block-style',
) );
}
add_action( 'init', 'custom_backup_block_init' );
/**
* Enqueue block assets for the editor.
*/
function custom_backup_block_editor_assets() {
// Enqueue SolidJS runtime and block script
wp_enqueue_script(
'custom-backup-block-editor-script',
plugin_dir_url( __FILE__ ) . 'build/index.js',
array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
);
// Enqueue editor-only styles if needed
// wp_enqueue_style(
// 'custom-backup-block-editor-style',
// plugin_dir_url( __FILE__ ) . 'build/index.css',
// array( 'wp-edit-blocks' ),
// filemtime( plugin_dir_path( __FILE__ ) . 'build/index.css' )
// );
}
add_action( 'enqueue_block_editor_assets', 'custom_backup_block_editor_assets' );
/**
* Enqueue block assets for the frontend.
*/
function custom_backup_block_frontend_assets() {
// Enqueue frontend styles if needed
// wp_enqueue_style(
// 'custom-backup-block-style',
// plugin_dir_url( __FILE__ ) . 'build/style-index.css',
// array(),
// filemtime( plugin_dir_path( __FILE__ ) . 'build/style-index.css' )
// );
}
add_action( 'enqueue_block_assets', 'custom_backup_block_frontend_assets' );
// Ensure backup directory is writable (this is a fallback, config should handle it)
if ( ! is_dir( WP_CONTENT_DIR . '/db_backups/' ) ) {
wp_mkdir_p( WP_CONTENT_DIR . '/db_backups/' );
}
src/block.json (Block Metadata)
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "custom-backup-block/backup-manager",
"version": "0.1.0",
"title": "Database Backup Manager",
"category": "widgets",
"icon": "database",
"description": "Manage and trigger database backups.",
"keywords": ["backup", "database", "ecommerce", "security"],
"attributes": {
"backupInterval": {
"type": "number",
"default": 24
}
},
"supports": {
"html": false
},
"textdomain": "custom-backup-block",
"editorScript": "file:./build/index.js",
"editorStyle": "file:./build/index.css",
"style": "file:./build/style-index.css"
}
package.json (Build Configuration)
{
"name": "custom-backup-block",
"version": "1.0.0",
"description": "A Gutenberg block for managing database backups using SolidJS.",
"main": "build/index.js",
"scripts": {
"build": "wp-scripts build --experimental-externals=react,react-dom",
"start": "wp-scripts start --experimental-externals=react,react-dom",
"packages-update": "wp-scripts packages-update"
},
"keywords": ["gutenberg", "block", "solidjs", "wordpress"],
"author": "Your Name",
"license": "GPL-2.0-or-later",
"devDependencies": {
"@wordpress/scripts": "^26.7.0",
"solid-js": "^1.8.15"
},
"dependencies": {
"@wordpress/components": "^26.7.0",
"@wordpress/element": "^5.4.0",
"@wordpress/i18n": "^4.44.0",
"@wordpress/blocks": "^12.10.0",
"@wordpress/editor": "^13.1.0"
}
}
Run npm install to install dependencies, then npm run build to compile the block assets. For development, use npm run start.
src/index.js (Block Registration)
This file registers the block and mounts our SolidJS application within the editor.
import { registerBlockType } from '@wordpress/blocks';
import { BackupManager } from './components/BackupManager';
import metadata from './block.json';
registerBlockType( metadata.name, {
edit: BackupManager,
save: () => null, // This block is purely for the editor interface
} );
src/components/BackupManager.jsx (SolidJS Component)
This is the core of our frontend. It uses SolidJS’s reactive primitives to fetch and display backup data, and to trigger actions.
import { createSignal, createEffect, Show } from 'solid-js';
import {
Button,
PanelBody,
Spinner,
Notice,
Table,
TableBody,
TableCell,
TableRow,
TextControl,
Flex,
FlexItem,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
// Define the REST API URL
const API_NAMESPACE = 'backup-engine/v1';
export function BackupManager() {
const [backups, setBackups] = createSignal([]);
const [isLoading, setIsLoading] = createSignal(false);
const [isGenerating, setIsGenerating] = createSignal(false);
const [notices, setNotices] = createSignal([]);
const [backupFileName, setBackupFileName] = createSignal(''); // For download/delete
// Function to add a notice
const addNotice = (status, message) => {
setNotices([...notices(), { status, message, id: Date.now() }]);
};
// Function to remove a notice
const removeNotice = (id) => {
setNotices(notices().filter(notice => notice.id !== id));
};
// Fetch backups on component mount
const fetchBackups = async () => {
setIsLoading(true);
setNotices([]); // Clear previous notices
try {
const response = await fetch(
`${window.location.origin}/wp-json/${API_NAMESPACE}/list`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success) {
setBackups(data.backups);
} else {
addNotice('error', __('Failed to load backups.', 'custom-backup-block'));
}
} catch (error) {
console.error('Error fetching backups:', error);
addNotice('error', __('An error occurred while fetching backups.', 'custom-backup-block'));
} finally {
setIsLoading(false);
}
};
// Trigger backup generation
const handleGenerateBackup = async () => {
setIsGenerating(true);
setNotices([]);
try {
const response = await fetch(
`${window.location.origin}/wp-json/${API_NAMESPACE}/generate`,
{ method: 'POST' }
);
const data = await response.json();
if (response.ok && data.success) {
addNotice('success', data.message || __('Backup generated successfully.', 'custom-backup-block'));
fetchBackups(); // Refresh the list
} else {
const errorMessage = data.message || data.error_message || __('Unknown error.', 'custom-backup-block');
addNotice('error', `${__('Backup generation failed:', 'custom-backup-block')} ${errorMessage}`);
}
} catch (error) {
console.error('Error generating backup:', error);
addNotice('error', __('An error occurred during backup generation.', 'custom-backup-block'));
} finally {
setIsGenerating(false);
}
};
// Handle download
const handleDownloadBackup = async (filename) => {
// Direct download via API endpoint
window.open(`${window.location.origin}/wp-json/${API_NAMESPACE}/download/${filename}`, '_blank');
addNotice('info', __('Download initiated. Check your downloads.', 'custom-backup-block'));
};
// Handle delete
const handleDeleteBackup = async (filename) => {
if (!confirm(__('Are you sure you want to delete this backup?', 'custom-backup-block'))) {
return;
}
try {
const response = await fetch(
`${window.location.origin}/wp-json/${API_NAMESPACE}/delete/${filename}`,
{ method: 'DELETE' }
);
const data = await response.json();
if (response.ok && data.success) {
addNotice('success', __('Backup deleted successfully.', 'custom-backup-block'));
fetchBackups(); // Refresh the list
} else {
const errorMessage = data.message || data.error_message || __('Unknown error.', 'custom-backup-block');
addNotice('error', `${__('Backup deletion failed:', 'custom-backup-block')} ${errorMessage}`);
}
} catch (error) {
console.error('Error deleting backup:', error);
addNotice('error', __('An error occurred during backup deletion.', 'custom-backup-block'));
}
};
// Effect to fetch backups when the component mounts
createEffect(() => {
fetchBackups();
});
return (
<PanelBody title={__('Database Backup Manager', 'custom-backup-block')} initialOpen={true}>
{/* Notices Area */}
<div className="backup-notices">
{notices().map(notice => (
<Notice
status={notice.status}
onRemove={() => removeNotice(notice.id)}
key={notice.id}
>
{notice.message}
</Notice>
))}
</div>
<div className="backup-actions">
<Button
variant="primary"
onClick={handleGenerateBackup}
disabled={isGenerating()}
isBusy={isGenerating()}
>
{__('Generate New Backup', 'custom-backup-block')}
</Button>
</div>
<h3>{__('Existing Backups', 'custom-backup-block')}</h3>
<Show when={isLoading()}>
<Spinner />
</Show>
<Show when={!isLoading() && backups().length === 0}>
<p>{__('No backups found.', 'custom-backup-block')}</p>
</Show>
<Show when={!isLoading() && backups().length > 0}>
<Table>
<TableBody>
<TableRow>
<TableCell><strong>{__('Filename', 'custom-backup-block')}</strong></TableCell>
<TableCell><strong>{__('Size', 'custom-backup-block')}</strong></TableCell>
<TableCell><strong>{__('Date', 'custom-backup-block')}</strong></TableCell>
<TableCell><strong>{__('Actions', 'custom-backup-block')}</strong></TableCell>
</TableRow>
{backups().map(backup => (
<TableRow key={backup.name}>
<TableCell>{backup.name}</TableCell>
<TableCell>{backup.size}</TableCell>
<TableCell>{backup.date}</TableCell>
<TableCell>
<Flex gap={8}>
<FlexItem>
<Button
icon="download"
label={__('Download', 'custom-backup-block')}
onClick={() => handleDownloadBackup(backup.name)}
isSmall
/>
</FlexItem>
<FlexItem>
<Button
icon="trash"
label={__('Delete', 'custom-backup-block')}
onClick={() => handleDeleteBackup(backup.name)}
isDestructive
isSmall
/>
</FlexItem>
</Flex>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Show>
</PanelBody>
);
}
Deployment and Usage
After setting up the plugin files and running npm run build, activate the “Custom Backup Block” plugin in your WordPress admin. You can then add the “Database Backup Manager” block to any page or post where you want administrators to access the backup controls. For optimal security, ensure the wp-content/db_backups/ directory is not publicly accessible via web server configuration (e.g., using .htaccess or Nginx directives).
Security Considerations
Database credentials should never be hardcoded directly in files that are committed to version control or are web-accessible. Consider using environment variables or WordPress’s secure configuration options. The backup directory must be protected from direct web access. Furthermore, ensure that only authorized users (e.g., administrators) can access the REST API endpoints by leveraging WordPress’s capability checks.
Performance and Scalability
For very large databases, the mysqldump process can be resource-intensive and time-consuming. Consider offloading backups to a separate cron job