feat: add PHP limit overrides and runtime extension checks
- BackupEngine: set_time_limit(0), memory_limit 512M, ignore_user_abort, flush output buffers, disable zlib compression - RestoreEngine: same PHP limit overrides - FtpUploader: check ext-ftp before upload attempt - GoogleDriveUploader: check ext-curl before upload attempt - BackupEngine: check ext-zip and ext-mbstring at startup - Kickstart restore.php: check ext-pdo_mysql and ext-mbstring in preflight, show max_execution_time and memory_limit in check results Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,16 @@ class BackupEngine
|
||||
*/
|
||||
public function run(int $profileId, string $description, string $origin = 'backend'): array
|
||||
{
|
||||
// Override PHP limits for long-running backup operations
|
||||
$this->overridePhpLimits();
|
||||
|
||||
// Verify required extensions
|
||||
$extCheck = $this->checkRequiredExtensions();
|
||||
|
||||
if ($extCheck !== true) {
|
||||
return ['success' => false, 'message' => $extCheck];
|
||||
}
|
||||
|
||||
$db = Factory::getDbo();
|
||||
|
||||
// Load profile
|
||||
@@ -221,6 +231,81 @@ class BackupEngine
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override PHP execution limits for backup operations.
|
||||
* Attempts multiple methods since some hosts restrict ini_set.
|
||||
*/
|
||||
private function overridePhpLimits(): void
|
||||
{
|
||||
// Remove execution time limit (0 = unlimited)
|
||||
@set_time_limit(0);
|
||||
@ini_set('max_execution_time', '0');
|
||||
|
||||
// Increase memory limit for large sites
|
||||
$currentMemory = $this->parseBytes(ini_get('memory_limit'));
|
||||
|
||||
if ($currentMemory > 0 && $currentMemory < 512 * 1024 * 1024) {
|
||||
@ini_set('memory_limit', '512M');
|
||||
}
|
||||
|
||||
// Disable output buffering to prevent memory buildup
|
||||
while (@ob_end_clean()) {
|
||||
// flush all output buffers
|
||||
}
|
||||
|
||||
// Prevent browser/proxy timeout by disabling compression
|
||||
@ini_set('zlib.output_compression', 'Off');
|
||||
|
||||
// Ignore user abort so backup completes even if browser closes
|
||||
@ignore_user_abort(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a PHP ini byte value (e.g. "128M") into bytes.
|
||||
*/
|
||||
private function parseBytes(string $value): int
|
||||
{
|
||||
$value = trim($value);
|
||||
|
||||
if ($value === '-1' || $value === '') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
$unit = strtolower(substr($value, -1));
|
||||
$num = (int) $value;
|
||||
|
||||
return match ($unit) {
|
||||
'g' => $num * 1024 * 1024 * 1024,
|
||||
'm' => $num * 1024 * 1024,
|
||||
'k' => $num * 1024,
|
||||
default => $num,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify required PHP extensions are loaded.
|
||||
*
|
||||
* @return true|string True if all ok, or error message string
|
||||
*/
|
||||
private function checkRequiredExtensions(): true|string
|
||||
{
|
||||
$missing = [];
|
||||
|
||||
if (!extension_loaded('zip')) {
|
||||
$missing[] = 'ext-zip (required for archive creation)';
|
||||
}
|
||||
|
||||
if (!extension_loaded('mbstring') && !function_exists('mb_strlen')) {
|
||||
$missing[] = 'ext-mbstring (required for binary-safe operations)';
|
||||
}
|
||||
|
||||
if (!empty($missing)) {
|
||||
return 'Missing PHP extensions: ' . implode(', ', $missing);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the appropriate remote uploader based on the storage type.
|
||||
*/
|
||||
|
||||
@@ -35,6 +35,10 @@ class FtpUploader implements RemoteUploaderInterface
|
||||
|
||||
public function upload(string $localPath, string $remoteName): array
|
||||
{
|
||||
if (!extension_loaded('ftp')) {
|
||||
return ['success' => false, 'message' => 'PHP ext-ftp is required for FTP uploads. Enable it in php.ini.'];
|
||||
}
|
||||
|
||||
if (empty($this->host)) {
|
||||
return ['success' => false, 'message' => 'FTP host is not configured'];
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@ class GoogleDriveUploader implements RemoteUploaderInterface
|
||||
|
||||
public function upload(string $localPath, string $remoteName): array
|
||||
{
|
||||
if (!extension_loaded('curl')) {
|
||||
return ['success' => false, 'message' => 'PHP ext-curl is required for Google Drive uploads. Enable it in php.ini.'];
|
||||
}
|
||||
|
||||
if (empty($this->clientId) || empty($this->refreshToken)) {
|
||||
return ['success' => false, 'message' => 'Google Drive credentials not configured'];
|
||||
}
|
||||
|
||||
@@ -111,11 +111,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
try {
|
||||
switch ($action) {
|
||||
case 'preflight':
|
||||
// Override PHP limits for restore
|
||||
@set_time_limit(0);
|
||||
@ini_set('max_execution_time', '0');
|
||||
@ini_set('memory_limit', '512M');
|
||||
@ignore_user_abort(true);
|
||||
|
||||
$checks = [];
|
||||
$checks['php_version'] = ['value' => PHP_VERSION, 'ok' => version_compare(PHP_VERSION, '8.1', '>=')];
|
||||
$checks['zip_ext'] = ['value' => extension_loaded('zip') ? 'Yes' : 'No', 'ok' => extension_loaded('zip')];
|
||||
$checks['pdo_mysql'] = ['value' => extension_loaded('pdo_mysql') ? 'Yes' : 'No', 'ok' => extension_loaded('pdo_mysql')];
|
||||
$checks['mbstring'] = ['value' => extension_loaded('mbstring') ? 'Yes' : 'No', 'ok' => extension_loaded('mbstring')];
|
||||
$checks['backup_exists'] = ['value' => file_exists(BACKUP_FILE) ? 'Yes' : 'No', 'ok' => file_exists(BACKUP_FILE)];
|
||||
$checks['writable'] = ['value' => is_writable(RESTORE_DIR) ? 'Yes' : 'No', 'ok' => is_writable(RESTORE_DIR)];
|
||||
$checks['max_execution_time'] = ['value' => ini_get('max_execution_time') ?: 'unlimited', 'ok' => true];
|
||||
$checks['memory_limit'] = ['value' => ini_get('memory_limit'), 'ok' => true];
|
||||
|
||||
if (file_exists(BACKUP_FILE)) {
|
||||
$checks['backup_size'] = ['value' => number_format(filesize(BACKUP_FILE) / 1048576, 2) . ' MB', 'ok' => true];
|
||||
|
||||
@@ -41,6 +41,16 @@ class RestoreEngine
|
||||
*/
|
||||
public function restore(int $recordId, bool $restoreFiles = true, bool $restoreDb = true, bool $preserveConfig = true): array
|
||||
{
|
||||
// Override PHP limits — restores can take a long time
|
||||
@set_time_limit(0);
|
||||
@ini_set('max_execution_time', '0');
|
||||
@ini_set('memory_limit', '512M');
|
||||
@ignore_user_abort(true);
|
||||
|
||||
if (!extension_loaded('zip')) {
|
||||
return ['success' => false, 'message' => 'PHP ext-zip is required for restore operations'];
|
||||
}
|
||||
|
||||
$db = Factory::getDbo();
|
||||
|
||||
// Load backup record
|
||||
|
||||
Reference in New Issue
Block a user