Files
Dapper/dapper_backup.php
carpentryplus25 9b408542af
All checks were successful
Generate Build Info / build-info (push) Successful in 1s
Automated Build Hash updates
2026-03-05 10:37:14 -05:00

408 lines
13 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Dapper Backup Module Low-Memory, Dated Folders Version
*/
if (!defined('ABSPATH')) {
exit;
}
define('DAPPER_BACKUP_BASE_DIR', WP_CONTENT_DIR . '/dapper-backups');
define('DAPPER_BACKUP_RETENTION', 7); // keep last 7 full backup sets
/**
* Ensure base backup directory exists and is protected
*/
function dapper_ensure_backup_dir() {
$base_dir = DAPPER_BACKUP_BASE_DIR;
if (!wp_mkdir_p($base_dir)) {
dapper_debug_log('Failed to create base backup dir: ' . $base_dir);
return false;
}
$htaccess = $base_dir . '/.htaccess';
if (!file_exists($htaccess)) {
file_put_contents($htaccess, "Order deny,allow\nDeny from all\n");
}
$index = $base_dir . '/index.php';
if (!file_exists($index)) {
file_put_contents($index, '<?php // Silence is golden');
}
return true;
}
/**
* Create a new dated backup subfolder and return its path
*/
function dapper_create_backup_folder() {
if (!dapper_ensure_backup_dir()) return false;
$timestamp = date('Y-m-d_H-i-s');
$backup_folder = DAPPER_BACKUP_BASE_DIR . '/' . $timestamp;
if (!wp_mkdir_p($backup_folder)) {
dapper_debug_log('Failed to create dated backup folder: ' . $backup_folder);
return false;
}
// Protect subfolder too
file_put_contents($backup_folder . '/index.php', '<?php // Silence is golden');
dapper_debug_log('Created dated backup folder: ' . $timestamp);
return $backup_folder;
}
/**
* Chunked database export to dated folder
*/
function dapper_export_database($backup_folder) {
global $wpdb;
$file = $backup_folder . '/database-' . date('Y-m-d_H-i-s') . '.sql';
$handle = fopen($file, 'w');
if ($handle === false) {
dapper_debug_log('Failed to open DB file: ' . $file);
return false;
}
fwrite($handle, "-- Dapper Database Backup\n-- Generated: " . date('Y-m-d H:i:s') . "\n\n");
$tables = $wpdb->get_col('SHOW TABLES');
$chunk_size = 300; // Smaller chunks for safety
foreach ($tables as $table) {
$create = $wpdb->get_row("SHOW CREATE TABLE `$table`", ARRAY_N);
fwrite($handle, $create[1] . ";\n\n");
$offset = 0;
while (true) {
$rows = $wpdb->get_results($wpdb->prepare("SELECT * FROM `$table` LIMIT %d OFFSET %d", $chunk_size, $offset), ARRAY_A);
if (empty($rows)) break;
foreach ($rows as $row) {
$values = array_map([$wpdb, '_real_escape'], array_values($row));
$insert = "INSERT INTO `$table` VALUES ('" . implode("','", $values) . "');\n";
fwrite($handle, $insert);
}
$offset += $chunk_size;
unset($rows);
gc_collect_cycles();
$mem = memory_get_usage(true) / 1024 / 1024;
if ($mem > 110) {
fclose($handle);
@unlink($file);
dapper_debug_log('Memory high during DB export — aborted');
return false;
}
}
fwrite($handle, "\n\n");
}
fclose($handle);
dapper_debug_log('DB backup written to: ' . basename($file));
return $file;
}
/*
* Memory-safe zip to dated folder (optional folders)
*/
function dapper_zip_essential_files($backup_folder) {
if (!dapper_ensure_backup_dir()) return false;
$zip_files = []; // Collect successful zips
$backup_folders = [];
if (get_option('dapper_backup_themes', 'on') === 'on') {
$backup_folders['themes'] = WP_CONTENT_DIR . '/themes';
}
if (get_option('dapper_backup_plugins', 'on') === 'on') {
$backup_folders['plugins'] = WP_CONTENT_DIR . '/plugins';
}
if (get_option('dapper_backup_include_media', 'off') === 'on') {
$backup_folders['uploads'] = WP_CONTENT_DIR . '/uploads';
}
if (empty($backup_folders)) {
dapper_debug_log('No folders selected for file backup');
return $zip_files;
}
foreach ($backup_folders as $type => $root) {
if (!is_dir($root)) {
dapper_debug_log('Folder not found, skipping: ' . $type);
continue;
}
$zip_file = $backup_folder . '/' . $type . '-' . date('Y-m-d_H-i-s') . '.zip';
$zip = new ZipArchive();
if ($zip->open($zip_file, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
dapper_debug_log('Failed to open ' . $type . ' zip: ' . $zip_file);
continue;
}
$file_count = 0;
$chunk_count = 0;
$max_per_chunk = 10000; // Reopen every 10k files to avoid timeout
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($iterator as $file) {
if ($file->isDir()) continue;
$file_path = $file->getRealPath();
$relative = substr($file_path, strlen(WP_CONTENT_DIR) + 1);
$zip->addFile($file_path, $relative);
$file_count++;
$chunk_count++;
if ($chunk_count % 500 === 0) {
$mem_mb = memory_get_usage(true) / 1024 / 1024;
dapper_debug_log("Added $file_count files to $type zip — memory: $mem_mb MB");
}
// Reopen zip every max_per_chunk files to reset buffers and avoid timeout
if ($chunk_count >= $max_per_chunk) {
$zip->close();
dapper_debug_log("Chunk complete for $type ($file_count total) — reopening zip");
$zip = new ZipArchive();
if ($zip->open($zip_file, ZipArchive::CREATE) !== true) { // CREATE appends, doesn't overwrite
dapper_debug_log('Failed to reopen ' . $type . ' zip');
break;
}
$chunk_count = 0; // Reset chunk counter
}
}
$zip->close();
if (!file_exists($zip_file) || filesize($zip_file) < 1024) {
dapper_debug_log($type . ' zip empty or failed: ' . $zip_file);
@unlink($zip_file);
continue;
}
dapper_debug_log($type . ' zip completed with ' . $file_count . ' files: ' . basename($zip_file));
$zip_files[] = $zip_file;
}
return $zip_files;
}
/**
* Create full dated backup set
*/
function dapper_create_new_backup() {
$backup_folder = dapper_create_backup_folder();
if (!$backup_folder) return false;
$db_file = dapper_export_database($backup_folder);
$files_zip = dapper_zip_essential_files($backup_folder);
if (!$db_file && !$files_zip) {
// Clean up empty folder if both failed
@rmdir($backup_folder);
return false;
}
dapper_cleanup_old_backups();
return $backup_folder;
}
/**
* Cleanup: keep only last N dated folders
*/
function dapper_cleanup_old_backups($keep = DAPPER_BACKUP_RETENTION) {
$folders = glob(DAPPER_BACKUP_BASE_DIR . '/*', GLOB_ONLYDIR);
if (count($folders) <= $keep) return;
usort($folders, function($a, $b) {
return filemtime($b) <=> filemtime($a);
});
for ($i = $keep; $i < count($folders); $i++) {
// Delete folder recursively
array_map('unlink', glob("$folders[$i]/*"));
@rmdir($folders[$i]);
dapper_debug_log('Deleted old backup folder: ' . basename($folders[$i]));
}
}
/**
* Get list of dated backups for display
*/
function dapper_get_old_backups() {
$folders = glob(DAPPER_BACKUP_BASE_DIR . '/*', GLOB_ONLYDIR);
$backups = [];
foreach ($folders as $folder) {
$timestamp = basename($folder);
$ts = strtotime(str_replace('_', ' ', $timestamp));
$db_file = glob($folder . '/database-*.sql');
$zip_file = glob($folder . '/files-*.zip');
$backups[] = [
'folder' => $folder,
'timestamp' => $ts,
'date_fmt' => date('Y-m-d H:i:s', $ts),
'db' => !empty($db_file) ? $db_file[0] : '',
'files' => !empty($zip_file) ? $zip_file[0] : '',
'size_db' => !empty($db_file) ? filesize($db_file[0]) : 0,
'size_files'=> !empty($zip_file) ? filesize($zip_file[0]) : 0,
];
}
// Sort newest first
usort($backups, function($a, $b) {
return $b['timestamp'] <=> $a['timestamp'];
});
return $backups;
}
/**
* Display backups in settings (dated folders)
*/
function display_old_backup_list() {
$folders = glob(DAPPER_BACKUP_BASE_DIR . '/*', GLOB_ONLYDIR);
if (empty($folders)) {
echo '<p>No backups yet.</p>';
return;
}
usort($folders, function($a, $b) { return filemtime($b) <=> filemtime($a); });
echo '<table class="widefat striped">';
echo '<thead><tr><th>Date</th><th>Files</th><th>Size</th><th>Actions</th></tr></thead>';
echo '<tbody>';
foreach ($folders as $folder) {
$date = date('Y-m-d H:i:s', filemtime($folder));
$files = glob($folder . '/*.{sql,zip}', GLOB_BRACE);
$total_size = 0;
$file_links = [];
foreach ($files as $file) {
$size = filesize($file);
$total_size += $size;
$fname = basename($file);
$nonce = wp_create_nonce('dapper_dl_' . md5($fname));
$link = admin_url('admin-post.php?action=dapper_backup_dl&f=' . urlencode($fname) . '&folder=' . urlencode(basename($folder)) . '&_wpnonce=' . $nonce);
$file_links[] = '<a href="' . esc_url($link) . '">' . esc_html($fname) . '</a> (' . size_format($size) . ')';
}
$delete_nonce = wp_create_nonce('dapper_delete_' . md5(basename($folder)));
$delete_link = admin_url('admin-post.php?action=dapper_delete_backup&folder=' . urlencode(basename($folder)) . '&_wpnonce=' . $delete_nonce);
echo '<tr>';
echo '<td>' . esc_html($date) . '</td>';
echo '<td>' . implode('<br>', $file_links) . '</td>';
echo '<td>' . size_format($total_size) . '</td>';
echo '<td><a href="' . esc_url($delete_link) . '" onclick="return confirm(\'Delete this backup set?\');">Delete</a></td>';
echo '</tr>';
}
echo '</tbody></table>';
}
// Secure download (updated for dated folders)
add_action('admin_post_dapper_backup_dl', 'dapper_handle_backup_download');
function dapper_handle_backup_download() {
if (!current_user_can('manage_options')) {
wp_die('Access denied.', 403);
}
$file = isset($_GET['f']) ? basename($_GET['f']) : '';
$folder = isset($_GET['folder']) ? basename($_GET['folder']) : '';
$nonce = $_GET['_wpnonce'] ?? '';
if (!wp_verify_nonce($nonce, 'dapper_dl_' . md5($file))) {
wp_die('Security check failed.', 403);
}
$path = DAPPER_BACKUP_BASE_DIR . '/' . $folder . '/' . $file;
if (!file_exists($path) || !is_readable($path)) {
wp_die('File not found or not readable.', 404);
}
// Kill all output buffering
while (ob_get_level() > 0) {
ob_end_clean();
}
// Turn off compression
if (function_exists('apache_setenv')) {
@apache_setenv('no-gzip', 1);
}
@ini_set('zlib.output_compression', 'Off');
// Headers
header('Content-Description: File Transfer');
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . $file . '";');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($path));
// Chunked read — very memory efficient
$chunk = 1 * 1024 * 1024; // 1 MB
$handle = @fopen($path, 'rb');
if ($handle === false) {
wp_die('Cannot open file for reading.');
}
while (!feof($handle)) {
echo @fread($handle, $chunk);
flush();
if (connection_aborted()) break;
}
@fclose($handle);
exit;
}
// Delete entire dated backup folder
add_action('admin_post_dapper_delete_backup', 'dapper_handle_delete_backup');
function dapper_handle_delete_backup() {
if (!current_user_can('manage_options')) wp_die('Access denied.');
$folder = isset($_GET['folder']) ? basename($_GET['folder']) : '';
$nonce = $_GET['_wpnonce'] ?? '';
if (!wp_verify_nonce($nonce, 'dapper_delete_' . md5($folder))) {
wp_die('Security check failed.');
}
$path = DAPPER_BACKUP_BASE_DIR . '/' . $folder;
if (!file_exists($path) || !is_dir($path)) {
wp_die('Backup not found.');
}
// Delete all files in folder
array_map('unlink', glob("$path/*"));
@rmdir($path);
dapper_debug_log('Deleted backup folder: ' . $folder);
$redirect = add_query_arg(
['page' => 'dapper-settings', 'backup_deleted' => '1'],
admin_url('admin.php')
);
wp_safe_redirect($redirect);
exit;
}