diff --git a/Panel/modules/administration/panel_update.php b/Panel/modules/administration/panel_update.php
index 7187edca..199aeadf 100644
--- a/Panel/modules/administration/panel_update.php
+++ b/Panel/modules/administration/panel_update.php
@@ -37,6 +37,7 @@ defined('GSP_DEFAULT_PANEL_SOURCE') || define('GSP_DEFAULT_PANEL_SOURCE', 'Panel
defined('GSP_DEFAULT_LINUX_AGENT_SOURCE') || define('GSP_DEFAULT_LINUX_AGENT_SOURCE', 'Agent_Linux');
defined('GSP_DEFAULT_WINDOWS_AGENT_SOURCE') || define('GSP_DEFAULT_WINDOWS_AGENT_SOURCE', 'Agent-Windows');
defined('GSP_DEFAULT_WEBSITE_SOURCE') || define('GSP_DEFAULT_WEBSITE_SOURCE', 'Website');
+defined('GSP_DEFAULT_BACKUP_RETENTION') || define('GSP_DEFAULT_BACKUP_RETENTION', 5);
$gspPatchManager = GSP_PANEL_DIR . '/modules/update/patch_manager.php';
if (file_exists($gspPatchManager)) {
@@ -53,6 +54,79 @@ $line = '[' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL;
@file_put_contents(GSP_UPDATE_LOG, $line, FILE_APPEND | LOCK_EX);
}
+function gsp_path_debug_summary($path)
+{
+$path = rtrim((string)$path, '/');
+if ($path === '') {
+return '[empty path]';
+}
+$parent = dirname($path);
+$parts = array(
+'path=' . $path,
+'exists=' . (file_exists($path) ? 'yes' : 'no'),
+'is_dir=' . (is_dir($path) ? 'yes' : 'no'),
+'writable=' . (is_writable($path) ? 'yes' : 'no'),
+'parent=' . $parent,
+'parent_exists=' . (file_exists($parent) ? 'yes' : 'no'),
+'parent_writable=' . (is_writable($parent) ? 'yes' : 'no'),
+);
+return implode(', ', $parts);
+}
+
+function gsp_ensure_directory($path, $label = 'directory')
+{
+$path = rtrim((string)$path, '/');
+if ($path === '' || strpos($path, "\0") !== false) {
+$message = 'Invalid ' . $label . ' path.';
+gsp_update_log($message . ' ' . gsp_path_debug_summary($path));
+return array('success' => false, 'error' => $message);
+}
+if (is_dir($path)) {
+if (!is_writable($path)) {
+$message = ucfirst($label) . ' exists but is not writable: ' . $path;
+gsp_update_log($message . ' [' . gsp_path_debug_summary($path) . ']');
+return array('success' => false, 'error' => $message);
+}
+return array('success' => true, 'path' => $path, 'created' => false);
+}
+if (!@mkdir($path, 0755, true) && !is_dir($path)) {
+$last = error_get_last();
+$detail = isset($last['message']) ? $last['message'] : 'mkdir returned false';
+$message = 'Cannot create ' . $label . ': ' . $path;
+gsp_update_log($message . ' [' . $detail . '] [' . gsp_path_debug_summary($path) . ']');
+return array('success' => false, 'error' => $message . ' (' . $detail . ')');
+}
+if (!is_writable($path)) {
+$message = ucfirst($label) . ' was created but is not writable: ' . $path;
+gsp_update_log($message . ' [' . gsp_path_debug_summary($path) . ']');
+return array('success' => false, 'error' => $message);
+}
+return array('success' => true, 'path' => $path, 'created' => true);
+}
+
+function gsp_get_backup_base(array $update_cfg = null)
+{
+$update_cfg = $update_cfg ?: gsp_update_settings();
+$base = !empty($update_cfg['backup_path']) ? rtrim((string)$update_cfg['backup_path'], '/') : GSP_BACKUP_BASE;
+if ($base === '' || strpos($base, "\0") !== false || strpos($base, '..') !== false || strpos($base, '/') !== 0) {
+$base = GSP_BACKUP_BASE;
+}
+return $base;
+}
+
+function gsp_get_backup_retention(array $update_cfg = null)
+{
+$update_cfg = $update_cfg ?: gsp_update_settings();
+$retention = isset($update_cfg['backup_retention']) ? (int)$update_cfg['backup_retention'] : GSP_DEFAULT_BACKUP_RETENTION;
+if ($retention < 1) {
+$retention = GSP_DEFAULT_BACKUP_RETENTION;
+}
+if ($retention > 200) {
+$retention = 200;
+}
+return $retention;
+}
+
function gsp_log_update_to_db($channel, $branch, $status, $message, $backup_path = null, $db_backup_path = null, $file_backup_path = null, $started_at = null, $finished_at = null)
{
global $db;
@@ -167,9 +241,10 @@ return [
'linux_agent_source_path' => !empty($settings['gsp_update_linux_agent_source_path']) ? trim((string)$settings['gsp_update_linux_agent_source_path'], '/') : GSP_DEFAULT_LINUX_AGENT_SOURCE,
'windows_agent_source_path' => !empty($settings['gsp_update_windows_agent_source_path']) ? trim((string)$settings['gsp_update_windows_agent_source_path'], '/') : GSP_DEFAULT_WINDOWS_AGENT_SOURCE,
'website_source_path' => !empty($settings['gsp_update_website_source_path']) ? trim((string)$settings['gsp_update_website_source_path'], '/') : GSP_DEFAULT_WEBSITE_SOURCE,
-'git_path' => !empty($settings['gsp_update_git_path']) ? trim((string)$settings['gsp_update_git_path']) : 'git',
-'backup_path' => !empty($settings['gsp_update_backup_path']) ? rtrim((string)$settings['gsp_update_backup_path'], '/') : GSP_BACKUP_BASE,
-'panel_post_update_command' => !empty($settings['gsp_update_panel_post_update_command']) ? (string)$settings['gsp_update_panel_post_update_command'] : '',
+ 'git_path' => !empty($settings['gsp_update_git_path']) ? trim((string)$settings['gsp_update_git_path']) : 'git',
+ 'backup_path' => !empty($settings['gsp_update_backup_path']) ? rtrim((string)$settings['gsp_update_backup_path'], '/') : GSP_BACKUP_BASE,
+ 'backup_retention' => !empty($settings['gsp_update_backup_retention']) ? (string)$settings['gsp_update_backup_retention'] : (string)GSP_DEFAULT_BACKUP_RETENTION,
+ 'panel_post_update_command' => !empty($settings['gsp_update_panel_post_update_command']) ? (string)$settings['gsp_update_panel_post_update_command'] : '',
'website_post_update_command' => !empty($settings['gsp_update_website_post_update_command']) ? (string)$settings['gsp_update_website_post_update_command'] : '',
'linux_agent_post_update_command' => !empty($settings['gsp_update_linux_agent_post_update_command']) ? (string)$settings['gsp_update_linux_agent_post_update_command'] : '',
'windows_agent_post_update_command' => !empty($settings['gsp_update_windows_agent_post_update_command']) ? (string)$settings['gsp_update_windows_agent_post_update_command'] : '',
@@ -200,11 +275,14 @@ if (trim((string)$cfg[$key]) === '' || strpos((string)$cfg[$key], "\0") !== fals
$errors[] = ucfirst(str_replace('_', ' ', $key)) . ' is invalid.';
}
}
-foreach (['website_path', 'backup_path'] as $key) {
-if (isset($cfg[$key]) && (trim((string)$cfg[$key]) === '' || strpos((string)$cfg[$key], "\0") !== false || strpos((string)$cfg[$key], '..') !== false || strpos((string)$cfg[$key], '/') !== 0)) {
-$errors[] = ucfirst(str_replace('_', ' ', $key)) . ' must be a safe absolute path.';
-}
-}
+ foreach (['website_path', 'backup_path'] as $key) {
+ if (isset($cfg[$key]) && (trim((string)$cfg[$key]) === '' || strpos((string)$cfg[$key], "\0") !== false || strpos((string)$cfg[$key], '..') !== false || strpos((string)$cfg[$key], '/') !== 0)) {
+ $errors[] = ucfirst(str_replace('_', ' ', $key)) . ' must be a safe absolute path.';
+ }
+ }
+ if (isset($cfg['backup_retention']) && (!preg_match('/^\d+$/', (string)$cfg['backup_retention']) || (int)$cfg['backup_retention'] < 1 || (int)$cfg['backup_retention'] > 200)) {
+ $errors[] = 'Backup retention must be a whole number between 1 and 200.';
+ }
foreach (['panel_source_path', 'linux_agent_source_path', 'windows_agent_source_path', 'website_source_path'] as $key) {
$value = isset($cfg[$key]) ? trim((string)$cfg[$key], '/') : '';
if ($value === '' || strpos($value, "\0") !== false || strpos($value, '..') !== false || !preg_match('/^[A-Za-z0-9._\/-]+$/', $value)) {
@@ -298,10 +376,11 @@ $warnings = [];
$update_cfg = $update_cfg ?: gsp_update_settings();
$cwd = getcwd();
$cwd_real = $cwd ? (realpath($cwd) ?: $cwd) : '';
-$root_path = rtrim((string)$update_cfg['repo_root'], '/');
-$panel_path = rtrim((string)$update_cfg['panel_path'], '/');
-$website_path = !empty($update_cfg['website_path']) ? rtrim((string)$update_cfg['website_path'], '/') : ($root_path . '/Website');
-$root_real = realpath($root_path) ?: $root_path;
+ $root_path = rtrim((string)$update_cfg['repo_root'], '/');
+ $panel_path = rtrim((string)$update_cfg['panel_path'], '/');
+ $website_path = !empty($update_cfg['website_path']) ? rtrim((string)$update_cfg['website_path'], '/') : ($root_path . '/Website');
+ $backup_base = gsp_get_backup_base($update_cfg);
+ $root_real = realpath($root_path) ?: $root_path;
$panel_real = realpath($panel_path) ?: $panel_path;
$website_real = realpath($website_path) ?: $website_path;
$layout = [
@@ -316,7 +395,7 @@ $layout = [
'panel_dir_real' => $panel_real,
'website_dir' => $website_path,
'website_dir_real' => $website_real,
-'backup_dir' => GSP_BACKUP_BASE,
+ 'backup_dir' => $backup_base,
'config_file' => $panel_path . '/includes/config.inc.php',
'destination_panel' => $panel_path,
'destination_website' => $website_path,
@@ -339,13 +418,12 @@ $warnings[] = 'Website directory is missing. Panel updates can still continue, b
if (!file_exists($layout['config_file'])) {
$errors[] = 'Panel includes/config.inc.php was not found and cannot be preserved.';
}
-if (!is_dir(GSP_BACKUP_BASE)) {
-if (!@mkdir(GSP_BACKUP_BASE, 0755, true) && !is_dir(GSP_BACKUP_BASE)) {
-$errors[] = 'Backups directory is missing and cannot be created.';
-}
-}
+ $backup_ready = gsp_ensure_directory($backup_base, 'backup directory');
+ if (!$backup_ready['success']) {
+ $errors[] = $backup_ready['error'];
+ }
-foreach ([$root_path, $panel_path, GSP_BACKUP_BASE] as $path) {
+ foreach ([$root_path, $panel_path, $backup_base] as $path) {
if (!is_writable($path)) {
$errors[] = 'Path is not writable: ' . $path;
}
@@ -518,33 +596,94 @@ $count++;
return ['success' => true, 'path' => $dest, 'count' => $count];
}
-function gsp_prune_old_backups($max_backups = 5)
+function gsp_get_managed_backup_entries($backup_base = null)
{
-$entries = gsp_get_available_backups();
-if (count($entries) <= $max_backups) {
-return;
-}
-$to_delete = array_slice($entries, $max_backups);
-foreach ($to_delete as $entry) {
-gsp_rmdir_recursive(GSP_BACKUP_BASE . '/' . $entry['ts']);
-gsp_update_log('Pruned old backup: ' . $entry['ts']);
-}
+ $backup_base = $backup_base ? rtrim((string)$backup_base, '/') : gsp_get_backup_base();
+ $entries = array();
+ if (!is_dir($backup_base)) {
+ return $entries;
+ }
+ foreach ((array)scandir($backup_base) as $entry) {
+ if ($entry === '.' || $entry === '..') {
+ continue;
+ }
+ $dir = $backup_base . '/' . $entry;
+ if (!is_dir($dir)) {
+ continue;
+ }
+ $type = null;
+ $stamp = null;
+ if (preg_match('/^(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})$/', $entry, $m)) {
+ $type = 'full';
+ $stamp = $m[1];
+ } elseif (preg_match('/^component_[A-Za-z0-9._-]+_(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})$/', $entry, $m)) {
+ $type = 'component';
+ $stamp = $m[1];
+ } else {
+ continue;
+ }
+ $meta_file = $dir . '/backup.json';
+ $meta = array();
+ if (is_file($meta_file)) {
+ $meta = json_decode((string)@file_get_contents($meta_file), true) ?: array();
+ }
+ $sort_time = strtotime(str_replace('_', ' ', $stamp));
+ if ($sort_time === false) {
+ $sort_time = @filemtime($dir) ?: 0;
+ }
+ $entries[] = array(
+ 'name' => $entry,
+ 'path' => $dir,
+ 'type' => $type,
+ 'ts' => $stamp,
+ 'sort_time' => $sort_time,
+ 'meta' => $meta,
+ );
+ }
+ usort($entries, function ($a, $b) {
+ if ($a['sort_time'] === $b['sort_time']) {
+ return strcmp($b['name'], $a['name']);
+ }
+ return ($a['sort_time'] < $b['sort_time']) ? 1 : -1;
+ });
+ return $entries;
}
-function gsp_create_full_backup($update_target_type, $update_target_version, $include_apache = false)
+function gsp_prune_old_backups($max_backups = 5, $backup_base = null)
{
-$ts = date('Y-m-d_H-i-s');
-$backup_dir = GSP_BACKUP_BASE . '/' . $ts;
-if (!is_dir(GSP_BACKUP_BASE) && !@mkdir(GSP_BACKUP_BASE, 0755, true)) {
-return ['success' => false, 'error' => 'Cannot create backup base directory.'];
-}
-if (!@mkdir($backup_dir, 0755, true)) {
-return ['success' => false, 'error' => 'Cannot create backup directory: ' . $backup_dir];
+ $entries = gsp_get_managed_backup_entries($backup_base);
+ if (count($entries) <= $max_backups) {
+ return array('success' => true, 'deleted' => array());
+ }
+ $to_delete = array_slice($entries, $max_backups);
+ $deleted = array();
+ foreach ($to_delete as $entry) {
+ gsp_rmdir_recursive($entry['path']);
+ $deleted[] = $entry['name'];
+ gsp_update_log('Pruned old backup: ' . $entry['path']);
+ }
+ return array('success' => true, 'deleted' => $deleted);
}
-$meta = [
-'backup_timestamp' => $ts,
-'gsp_root' => GSP_ROOT_DIR,
+function gsp_create_full_backup($update_target_type, $update_target_version, $include_apache = false, array $update_cfg = null)
+{
+ $ts = date('Y-m-d_H-i-s');
+ $backup_base = gsp_get_backup_base($update_cfg);
+ $retention = gsp_get_backup_retention($update_cfg);
+ $backup_base_ready = gsp_ensure_directory($backup_base, 'backup base directory');
+ if (!$backup_base_ready['success']) {
+ return array('success' => false, 'error' => $backup_base_ready['error']);
+ }
+ $backup_dir = $backup_base . '/' . $ts;
+ $backup_dir_ready = gsp_ensure_directory($backup_dir, 'backup directory');
+ if (!$backup_dir_ready['success']) {
+ return array('success' => false, 'error' => $backup_dir_ready['error']);
+ }
+
+ $meta = [
+ 'backup_timestamp' => $ts,
+ 'backup_base' => $backup_base,
+ 'gsp_root' => GSP_ROOT_DIR,
'panel_root' => GSP_PANEL_DIR,
'website_root' => GSP_WEBSITE_DIR,
'update_target_type' => $update_target_type,
@@ -593,17 +732,21 @@ $meta['apache_backup'] = $apache_backup['path'];
} else {
$meta['apache_backup_error'] = $apache_backup['error'];
}
-}
+ }
-@file_put_contents($backup_dir . '/backup.json', json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
-gsp_prune_old_backups(5);
-gsp_update_log('Backup created: ' . $backup_dir);
+ @file_put_contents($backup_dir . '/backup.json', json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
+ $prune = gsp_prune_old_backups($retention, $backup_base);
+ if (!empty($prune['deleted'])) {
+ gsp_update_log('Backup retention pruned ' . count($prune['deleted']) . ' entries from ' . $backup_base . ': ' . implode(', ', $prune['deleted']));
+ }
+ gsp_update_log('Backup created: ' . $backup_dir);
-return [
-'success' => true,
-'backup_dir' => $backup_dir,
-'backup_ts' => $ts,
-];
+ return [
+ 'success' => true,
+ 'backup_base' => $backup_base,
+ 'backup_dir' => $backup_dir,
+ 'backup_ts' => $ts,
+ ];
}
function gsp_download_zip($repo_owner, $repo_name, $ref, $temp_dir)
@@ -1047,13 +1190,63 @@ $nonce = gsp_random_token(12);
$_SESSION['gsp_update_restart_nonce'] = $nonce;
gsp_update_log('Updater self-update applied (' . $copied . ' files); restart nonce=' . $nonce);
gsp_rmdir_recursive($temp_dir);
-return [
-'success' => false,
-'restart_required' => true,
-'restart_nonce' => $nonce,
-'updater_files_updated' => $copied,
-'drift_files' => $drift_files,
-];
+ return [
+ 'success' => false,
+ 'restart_required' => true,
+ 'restart_nonce' => $nonce,
+ 'updater_files_updated' => $copied,
+ 'drift_files' => $drift_files,
+ ];
+ }
+
+ if (!empty($restart_nonce)) {
+ $expected = isset($_SESSION['gsp_update_restart_nonce']) ? $_SESSION['gsp_update_restart_nonce'] : null;
+ if ($expected === null || !hash_equals($expected, $restart_nonce)) {
+ gsp_rmdir_recursive($temp_dir);
+ return ['success' => false, 'error' => 'Invalid updater restart marker.'];
+ }
+ unset($_SESSION['gsp_update_restart_nonce']);
+ }
+
+ $patches = gsp_run_required_patches($updater_version);
+ if (!$patches['success']) {
+ gsp_rmdir_recursive($temp_dir);
+ return ['success' => false, 'error' => $patches['error']];
+ }
+
+ $sync = gsp_apply_layout_sync($layout);
+ gsp_rmdir_recursive($temp_dir);
+ if (!$sync['success']) {
+ return $sync;
+ }
+ $sync_validation = gsp_validate_layout_sync_result($layout, $sync);
+ if (!$sync_validation['success']) {
+ return ['success' => false, 'error' => 'Deployed file validation failed: ' . implode(' | ', $sync_validation['errors'])];
+ }
+ gsp_update_log('Layout sync complete: copied=' . $sync['files_copied'] . ', skipped=' . count($sync['skipped']));
+ gsp_update_log('Layout sync totals: Panel=' . intval($sync['panel_files_copied']) . ', Website=' . intval($sync['website_files_copied']));
+ if (!empty($sync['skipped'])) {
+ gsp_update_log('Preserved paths: ' . implode(', ', array_slice($sync['skipped'], 0, 50)));
+ }
+ if (!empty($sync['copied_files'])) {
+ $copied_sample = array_slice((array)$sync['copied_files'], 0, 50);
+ gsp_update_log('Copied file sample: ' . implode(', ', $copied_sample));
+ $addons_updates = array_values(array_filter((array)$sync['copied_files'], function ($rel) {
+ return strpos($rel, 'Panel/modules/addonsmanager/') === 0;
+ }));
+ if (!empty($addons_updates)) {
+ gsp_update_log('Addonsmanager files copied: ' . implode(', ', array_slice($addons_updates, 0, 50)));
+ }
+ }
+ return [
+ 'success' => true,
+ 'files_copied' => $sync['files_copied'],
+ 'panel_files_copied' => $sync['panel_files_copied'],
+ 'website_files_copied' => $sync['website_files_copied'],
+ 'preserved' => $sync['skipped'],
+ 'copied_files' => $sync['copied_files'],
+ 'patches' => $patches['run'],
+ ];
}
function gsp_checkout_update_source(array $update_cfg)
@@ -1145,55 +1338,6 @@ return [
'patches' => $patches['run'],
];
}
-if (!empty($restart_nonce)) {
-$expected = isset($_SESSION['gsp_update_restart_nonce']) ? $_SESSION['gsp_update_restart_nonce'] : null;
-if ($expected === null || !hash_equals($expected, $restart_nonce)) {
-gsp_rmdir_recursive($temp_dir);
-return ['success' => false, 'error' => 'Invalid updater restart marker.'];
-}
-unset($_SESSION['gsp_update_restart_nonce']);
-}
-
-$patches = gsp_run_required_patches($updater_version);
-if (!$patches['success']) {
-gsp_rmdir_recursive($temp_dir);
-return ['success' => false, 'error' => $patches['error']];
-}
-
-$sync = gsp_apply_layout_sync($layout);
-gsp_rmdir_recursive($temp_dir);
-if (!$sync['success']) {
-return $sync;
-}
-$sync_validation = gsp_validate_layout_sync_result($layout, $sync);
-if (!$sync_validation['success']) {
-return ['success' => false, 'error' => 'Deployed file validation failed: ' . implode(' | ', $sync_validation['errors'])];
-}
-gsp_update_log('Layout sync complete: copied=' . $sync['files_copied'] . ', skipped=' . count($sync['skipped']));
-gsp_update_log('Layout sync totals: Panel=' . intval($sync['panel_files_copied']) . ', Website=' . intval($sync['website_files_copied']));
-if (!empty($sync['skipped'])) {
-gsp_update_log('Preserved paths: ' . implode(', ', array_slice($sync['skipped'], 0, 50)));
-}
-if (!empty($sync['copied_files'])) {
-$copied_sample = array_slice((array)$sync['copied_files'], 0, 50);
-gsp_update_log('Copied file sample: ' . implode(', ', $copied_sample));
-$addons_updates = array_values(array_filter((array)$sync['copied_files'], function ($rel) {
-return strpos($rel, 'Panel/modules/addonsmanager/') === 0;
-}));
-if (!empty($addons_updates)) {
-gsp_update_log('Addonsmanager files copied: ' . implode(', ', array_slice($addons_updates, 0, 50)));
-}
-}
-return [
-'success' => true,
-'files_copied' => $sync['files_copied'],
-'panel_files_copied' => $sync['panel_files_copied'],
-'website_files_copied' => $sync['website_files_copied'],
-'preserved' => $sync['skipped'],
-'copied_files' => $sync['copied_files'],
-'patches' => $patches['run'],
-];
-}
function gsp_fix_permissions($root_dir)
{
@@ -1299,12 +1443,12 @@ if (!$preflight['success']) {
return ['success' => false, 'error' => 'Preflight failed: ' . implode(' | ', $preflight['errors'])];
}
-$backup = ['success' => true, 'backup_dir' => null];
-if (!empty($update_cfg['backup_before_update'])) {
-$backup = gsp_create_full_backup('git-update', $update_cfg['branch'], false);
-if (!$backup['success']) {
-return $backup;
-}
+ $backup = ['success' => true, 'backup_dir' => null];
+ if (!empty($update_cfg['backup_before_update'])) {
+ $backup = gsp_create_full_backup('git-update', $update_cfg['branch'], false, $update_cfg);
+ if (!$backup['success']) {
+ return $backup;
+ }
gsp_update_log("Backup created before git update to {$update_cfg['branch']}: {$backup['backup_dir']}");
}
@@ -1354,21 +1498,23 @@ return [
];
}
-function gsp_safe_component_backup($component, $source_path, $backup_base)
+function gsp_safe_component_backup($component, $source_path, $backup_base, $retention = GSP_DEFAULT_BACKUP_RETENTION)
{
-$component = preg_replace('/[^A-Za-z0-9._-]/', '_', (string)$component);
-$backup_base = rtrim((string)$backup_base, '/');
-if ($backup_base === '' || strpos($backup_base, '/') !== 0) {
-$backup_base = GSP_BACKUP_BASE;
-}
-if (!is_dir($backup_base) && !@mkdir($backup_base, 0755, true)) {
-return ['success' => false, 'error' => 'Cannot create backup path: ' . $backup_base];
-}
-$ts = date('Y-m-d_H-i-s');
-$backup_dir = $backup_base . '/component_' . $component . '_' . $ts;
-if (!@mkdir($backup_dir, 0755, true)) {
-return ['success' => false, 'error' => 'Cannot create component backup directory: ' . $backup_dir];
-}
+ $component = preg_replace('/[^A-Za-z0-9._-]/', '_', (string)$component);
+ $backup_base = rtrim((string)$backup_base, '/');
+ if ($backup_base === '' || strpos($backup_base, '/') !== 0) {
+ $backup_base = gsp_get_backup_base();
+ }
+ $backup_base_ready = gsp_ensure_directory($backup_base, 'component backup path');
+ if (!$backup_base_ready['success']) {
+ return ['success' => false, 'error' => $backup_base_ready['error']];
+ }
+ $ts = date('Y-m-d_H-i-s');
+ $backup_dir = $backup_base . '/component_' . $component . '_' . $ts;
+ $backup_dir_ready = gsp_ensure_directory($backup_dir, 'component backup directory');
+ if (!$backup_dir_ready['success']) {
+ return ['success' => false, 'error' => $backup_dir_ready['error']];
+ }
if (!is_dir($source_path)) {
return ['success' => true, 'backup_dir' => $backup_dir, 'skipped' => true];
}
@@ -1380,10 +1526,14 @@ return $archive_result;
@file_put_contents($backup_dir . '/backup.json', json_encode([
'component' => $component,
'source_path' => $source_path,
-'created_at' => date('Y-m-d H:i:s'),
-'archive' => basename($archive),
-], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
-return ['success' => true, 'backup_dir' => $backup_dir, 'archive' => $archive];
+ 'created_at' => date('Y-m-d H:i:s'),
+ 'archive' => basename($archive),
+ ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
+ $prune = gsp_prune_old_backups((int)$retention, $backup_base);
+ if (!empty($prune['deleted'])) {
+ gsp_update_log('Component backup retention pruned ' . count($prune['deleted']) . ' entries from ' . $backup_base . ': ' . implode(', ', $prune['deleted']));
+ }
+ return ['success' => true, 'backup_dir' => $backup_dir, 'archive' => $archive];
}
function gsp_component_source_path($source_root, $relative)
@@ -1518,7 +1668,7 @@ return ['success' => false, 'error' => 'Destination path for ' . $component . '
}
$backup = ['success' => true, 'backup_dir' => null];
if (!empty($update_cfg['backup_before_update'])) {
-$backup = gsp_safe_component_backup($component, $dest, $update_cfg['backup_path']);
+ $backup = gsp_safe_component_backup($component, $dest, gsp_get_backup_base($update_cfg), gsp_get_backup_retention($update_cfg));
if (!$backup['success']) {
gsp_rmdir_recursive($checkout['temp_dir']);
return $backup;
@@ -1680,17 +1830,18 @@ gsp_update_log('Remote agent update requested: id=' . $remote_id . ' component='
return ['success' => true, 'results' => $results, 'success_count' => $success_count];
}
-function gsp_get_available_backups()
+function gsp_get_available_backups($backup_base = null)
{
-$backups = [];
-if (!is_dir(GSP_BACKUP_BASE)) {
-return $backups;
-}
-foreach ((array)scandir(GSP_BACKUP_BASE) as $entry) {
+ $backups = [];
+ $backup_base = $backup_base ? rtrim((string)$backup_base, '/') : gsp_get_backup_base();
+ if (!is_dir($backup_base)) {
+ return $backups;
+ }
+ foreach ((array)scandir($backup_base) as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
-$dir = GSP_BACKUP_BASE . '/' . $entry;
+ $dir = $backup_base . '/' . $entry;
if (!is_dir($dir)) {
continue;
}
@@ -1763,10 +1914,10 @@ return ['success' => false, 'error' => 'MySQL restore failed: ' . implode(' | ',
return ['success' => true];
}
-function gsp_do_revert($backup_ts, $restore_apache = false)
+function gsp_do_revert($backup_ts, $restore_apache = false, array $update_cfg = null)
{
-global $db;
-$backup_dir = GSP_BACKUP_BASE . '/' . $backup_ts;
+ global $db;
+ $backup_dir = gsp_get_backup_base($update_cfg) . '/' . $backup_ts;
if (!is_dir($backup_dir)) {
return ['success' => false, 'error' => 'Backup not found: ' . $backup_ts];
}
@@ -2058,37 +2209,12 @@ return [
function gsp_fix_apache_paths($confirmed, $reload_apache)
{
-if (!$confirmed) {
-return ['success' => false, 'error' => 'Apache path fix requires explicit confirmation.'];
-}
-
-function gsp_disable_ssl_vhost($vhost_file, $confirmed)
-{
-if (!$confirmed) {
-return ['success' => false, 'error' => 'SSL vhost disable requires confirmation.'];
-}
-$vhost = basename((string)$vhost_file);
-if (!preg_match('/\.conf$/', $vhost) || strpos($vhost, '-ssl') === false) {
-return ['success' => false, 'error' => 'Only SSL vhost .conf files can be disabled from this action.'];
-}
-$enabled_path = '/etc/apache2/sites-enabled/' . $vhost;
-if (!file_exists($enabled_path) && !is_link($enabled_path)) {
-return ['success' => true, 'message' => $vhost . ' is already disabled.'];
-}
-if (!@unlink($enabled_path)) {
-return ['success' => false, 'error' => 'Failed to disable SSL site: ' . $vhost];
-}
-$test = gsp_apache_configtest();
-if (!$test['success']) {
-return ['success' => false, 'error' => 'Disabled site but apache2ctl configtest still failed: ' . $test['output']];
-}
-$reload = gsp_apache_reload();
-gsp_update_log('Disabled SSL vhost in sites-enabled: ' . $vhost);
-return ['success' => true, 'configtest' => $test, 'reload' => $reload, 'message' => 'Disabled SSL vhost: ' . $vhost];
-}
-$scan = gsp_scan_apache_configs();
-if (!$scan['available']) {
-return ['success' => false, 'error' => 'Apache config folder not available.'];
+ if (!$confirmed) {
+ return ['success' => false, 'error' => 'Apache path fix requires explicit confirmation.'];
+ }
+ $scan = gsp_scan_apache_configs();
+ if (!$scan['available']) {
+ return ['success' => false, 'error' => 'Apache config folder not available.'];
}
$backup = gsp_create_full_backup('apache-fix', 'apache-path-repair', true);
@@ -2174,8 +2300,33 @@ return [
'backup_dir' => $backup['backup_dir'],
'configtest' => $test,
'reload' => $reload,
-'planned_replacements' => $planned,
-];
+ 'planned_replacements' => $planned,
+ ];
+}
+
+function gsp_disable_ssl_vhost($vhost_file, $confirmed)
+{
+ if (!$confirmed) {
+ return ['success' => false, 'error' => 'SSL vhost disable requires confirmation.'];
+ }
+ $vhost = basename((string)$vhost_file);
+ if (!preg_match('/\.conf$/', $vhost) || strpos($vhost, '-ssl') === false) {
+ return ['success' => false, 'error' => 'Only SSL vhost .conf files can be disabled from this action.'];
+ }
+ $enabled_path = '/etc/apache2/sites-enabled/' . $vhost;
+ if (!file_exists($enabled_path) && !is_link($enabled_path)) {
+ return ['success' => true, 'message' => $vhost . ' is already disabled.'];
+ }
+ if (!@unlink($enabled_path)) {
+ return ['success' => false, 'error' => 'Failed to disable SSL site: ' . $vhost];
+ }
+ $test = gsp_apache_configtest();
+ if (!$test['success']) {
+ return ['success' => false, 'error' => 'Disabled site but apache2ctl configtest still failed: ' . $test['output']];
+ }
+ $reload = gsp_apache_reload();
+ gsp_update_log('Disabled SSL vhost in sites-enabled: ' . $vhost);
+ return ['success' => true, 'configtest' => $test, 'reload' => $reload, 'message' => 'Disabled SSL vhost: ' . $vhost];
}
function gsp_get_patch_overview()
@@ -2255,10 +2406,11 @@ $submitted_csrf = isset($_POST['gsp_update_csrf']) ? $_POST['gsp_update_csrf'] :
if (!hash_equals($csrf_token, $submitted_csrf)) {
print_failure('Invalid security token. Please reload and try again.');
} else {
-$action = $_POST['gsp_update_action'];
-$started_at = date('Y-m-d H:i:s');
-$restart_nonce = isset($_POST['gsp_restart_nonce']) ? trim($_POST['gsp_restart_nonce']) : '';
-set_time_limit(0);
+ $action = $_POST['gsp_update_action'];
+ $started_at = date('Y-m-d H:i:s');
+ $restart_nonce = isset($_POST['gsp_restart_nonce']) ? trim($_POST['gsp_restart_nonce']) : '';
+ set_time_limit(0);
+ try {
if ($action === 'preflight') {
$preflight_result = gsp_preflight_check();
@@ -2297,8 +2449,8 @@ print_success(htmlspecialchars(isset($disable['message']) ? $disable['message']
} else {
print_failure('Disable SSL vhost failed: ' . htmlspecialchars($disable['error']));
}
-} elseif ($action === 'backup_only') {
-$result = gsp_create_full_backup('backup-only', 'manual', false);
+ } elseif ($action === 'backup_only') {
+ $result = gsp_create_full_backup('backup-only', 'manual', false, $update_cfg);
if ($result['success']) {
print_success('Backup created: ' . htmlspecialchars($result['backup_dir']) . '');
} else {
@@ -2315,9 +2467,10 @@ $new_cfg = [
'linux_agent_source_path' => isset($_POST['gsp_update_linux_agent_source_path']) ? trim((string)$_POST['gsp_update_linux_agent_source_path'], '/') : '',
'windows_agent_source_path' => isset($_POST['gsp_update_windows_agent_source_path']) ? trim((string)$_POST['gsp_update_windows_agent_source_path'], '/') : '',
'website_source_path' => isset($_POST['gsp_update_website_source_path']) ? trim((string)$_POST['gsp_update_website_source_path'], '/') : '',
-'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : '',
-'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : '',
-'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : '',
+ 'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : '',
+ 'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : '',
+ 'backup_retention' => isset($_POST['gsp_update_backup_retention']) ? trim((string)$_POST['gsp_update_backup_retention']) : '',
+ 'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : '',
'website_post_update_command' => isset($_POST['gsp_update_website_post_update_command']) ? trim((string)$_POST['gsp_update_website_post_update_command']) : '',
'linux_agent_post_update_command' => isset($_POST['gsp_update_linux_agent_post_update_command']) ? trim((string)$_POST['gsp_update_linux_agent_post_update_command']) : '',
'windows_agent_post_update_command' => isset($_POST['gsp_update_windows_agent_post_update_command']) ? trim((string)$_POST['gsp_update_windows_agent_post_update_command']) : '',
@@ -2337,9 +2490,10 @@ $db->setSettings([
'gsp_update_linux_agent_source_path' => $new_cfg['linux_agent_source_path'],
'gsp_update_windows_agent_source_path' => $new_cfg['windows_agent_source_path'],
'gsp_update_website_source_path' => $new_cfg['website_source_path'],
-'gsp_update_git_path' => $new_cfg['git_path'],
-'gsp_update_backup_path' => $new_cfg['backup_path'],
-'gsp_update_panel_post_update_command' => $new_cfg['panel_post_update_command'],
+ 'gsp_update_git_path' => $new_cfg['git_path'],
+ 'gsp_update_backup_path' => $new_cfg['backup_path'],
+ 'gsp_update_backup_retention' => $new_cfg['backup_retention'],
+ 'gsp_update_panel_post_update_command' => $new_cfg['panel_post_update_command'],
'gsp_update_website_post_update_command' => $new_cfg['website_post_update_command'],
'gsp_update_linux_agent_post_update_command' => $new_cfg['linux_agent_post_update_command'],
'gsp_update_windows_agent_post_update_command' => $new_cfg['windows_agent_post_update_command'],
@@ -2354,9 +2508,10 @@ $settings['gsp_update_panel_source_path'] = $new_cfg['panel_source_path'];
$settings['gsp_update_linux_agent_source_path'] = $new_cfg['linux_agent_source_path'];
$settings['gsp_update_windows_agent_source_path'] = $new_cfg['windows_agent_source_path'];
$settings['gsp_update_website_source_path'] = $new_cfg['website_source_path'];
-$settings['gsp_update_git_path'] = $new_cfg['git_path'];
-$settings['gsp_update_backup_path'] = $new_cfg['backup_path'];
-$settings['gsp_update_panel_post_update_command'] = $new_cfg['panel_post_update_command'];
+ $settings['gsp_update_git_path'] = $new_cfg['git_path'];
+ $settings['gsp_update_backup_path'] = $new_cfg['backup_path'];
+ $settings['gsp_update_backup_retention'] = $new_cfg['backup_retention'];
+ $settings['gsp_update_panel_post_update_command'] = $new_cfg['panel_post_update_command'];
$settings['gsp_update_website_post_update_command'] = $new_cfg['website_post_update_command'];
$settings['gsp_update_linux_agent_post_update_command'] = $new_cfg['linux_agent_post_update_command'];
$settings['gsp_update_windows_agent_post_update_command'] = $new_cfg['windows_agent_post_update_command'];
@@ -2375,9 +2530,10 @@ $update_cfg = [
'linux_agent_source_path' => isset($_POST['gsp_update_linux_agent_source_path']) ? trim((string)$_POST['gsp_update_linux_agent_source_path'], '/') : $update_cfg['linux_agent_source_path'],
'windows_agent_source_path' => isset($_POST['gsp_update_windows_agent_source_path']) ? trim((string)$_POST['gsp_update_windows_agent_source_path'], '/') : $update_cfg['windows_agent_source_path'],
'website_source_path' => isset($_POST['gsp_update_website_source_path']) ? trim((string)$_POST['gsp_update_website_source_path'], '/') : $update_cfg['website_source_path'],
-'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : $update_cfg['git_path'],
-'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : $update_cfg['backup_path'],
-'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : $update_cfg['panel_post_update_command'],
+ 'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : $update_cfg['git_path'],
+ 'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : $update_cfg['backup_path'],
+ 'backup_retention' => isset($_POST['gsp_update_backup_retention']) ? trim((string)$_POST['gsp_update_backup_retention']) : $update_cfg['backup_retention'],
+ 'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : $update_cfg['panel_post_update_command'],
'website_post_update_command' => isset($_POST['gsp_update_website_post_update_command']) ? trim((string)$_POST['gsp_update_website_post_update_command']) : $update_cfg['website_post_update_command'],
'linux_agent_post_update_command' => isset($_POST['gsp_update_linux_agent_post_update_command']) ? trim((string)$_POST['gsp_update_linux_agent_post_update_command']) : $update_cfg['linux_agent_post_update_command'],
'windows_agent_post_update_command' => isset($_POST['gsp_update_windows_agent_post_update_command']) ? trim((string)$_POST['gsp_update_windows_agent_post_update_command']) : $update_cfg['windows_agent_post_update_command'],
@@ -2442,9 +2598,10 @@ $update_cfg = [
'linux_agent_source_path' => isset($_POST['gsp_update_linux_agent_source_path']) ? trim((string)$_POST['gsp_update_linux_agent_source_path'], '/') : $update_cfg['linux_agent_source_path'],
'windows_agent_source_path' => isset($_POST['gsp_update_windows_agent_source_path']) ? trim((string)$_POST['gsp_update_windows_agent_source_path'], '/') : $update_cfg['windows_agent_source_path'],
'website_source_path' => isset($_POST['gsp_update_website_source_path']) ? trim((string)$_POST['gsp_update_website_source_path'], '/') : $update_cfg['website_source_path'],
-'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : $update_cfg['git_path'],
-'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : $update_cfg['backup_path'],
-'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : $update_cfg['panel_post_update_command'],
+ 'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : $update_cfg['git_path'],
+ 'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : $update_cfg['backup_path'],
+ 'backup_retention' => isset($_POST['gsp_update_backup_retention']) ? trim((string)$_POST['gsp_update_backup_retention']) : $update_cfg['backup_retention'],
+ 'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : $update_cfg['panel_post_update_command'],
'website_post_update_command' => isset($_POST['gsp_update_website_post_update_command']) ? trim((string)$_POST['gsp_update_website_post_update_command']) : $update_cfg['website_post_update_command'],
'linux_agent_post_update_command' => isset($_POST['gsp_update_linux_agent_post_update_command']) ? trim((string)$_POST['gsp_update_linux_agent_post_update_command']) : $update_cfg['linux_agent_post_update_command'],
'windows_agent_post_update_command' => isset($_POST['gsp_update_windows_agent_post_update_command']) ? trim((string)$_POST['gsp_update_windows_agent_post_update_command']) : $update_cfg['windows_agent_post_update_command'],
@@ -2460,9 +2617,10 @@ $db->setSettings([
'gsp_update_linux_agent_source_path' => $update_cfg['linux_agent_source_path'],
'gsp_update_windows_agent_source_path' => $update_cfg['windows_agent_source_path'],
'gsp_update_website_source_path' => $update_cfg['website_source_path'],
-'gsp_update_git_path' => $update_cfg['git_path'],
-'gsp_update_backup_path' => $update_cfg['backup_path'],
-'gsp_update_panel_post_update_command' => $update_cfg['panel_post_update_command'],
+ 'gsp_update_git_path' => $update_cfg['git_path'],
+ 'gsp_update_backup_path' => $update_cfg['backup_path'],
+ 'gsp_update_backup_retention' => $update_cfg['backup_retention'],
+ 'gsp_update_panel_post_update_command' => $update_cfg['panel_post_update_command'],
'gsp_update_website_post_update_command' => $update_cfg['website_post_update_command'],
'gsp_update_linux_agent_post_update_command' => $update_cfg['linux_agent_post_update_command'],
'gsp_update_windows_agent_post_update_command' => $update_cfg['windows_agent_post_update_command'],
@@ -2526,32 +2684,41 @@ $restore_apache = !empty($_POST['gsp_restore_apache']);
if (!preg_match('/^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$/', $backup_ts)) {
print_failure('Invalid backup timestamp selected.');
} else {
-$result = gsp_do_revert($backup_ts, $restore_apache);
+ $result = gsp_do_revert($backup_ts, $restore_apache, $update_cfg);
if ($result['success']) {
print_success('Reverted to backup ' . htmlspecialchars($backup_ts) . '.');
} else {
print_failure('Revert failed: ' . htmlspecialchars($result['error']));
}
}
-}
+ }
-$finished_at = date('Y-m-d H:i:s');
-gsp_log_update_to_db('panel-update', null, 'info', 'Admin action: ' . $action, null, null, null, $started_at, $finished_at);
-}
+ $finished_at = date('Y-m-d H:i:s');
+ gsp_log_update_to_db('panel-update', null, 'info', 'Admin action: ' . $action, null, null, null, $started_at, $finished_at);
+ } catch (Throwable $e) {
+ $finished_at = date('Y-m-d H:i:s');
+ gsp_update_log('Update action failure [' . $action . ']: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
+ gsp_log_update_to_db('panel-update', null, 'error', 'Admin action failed: ' . $action . ' | ' . $e->getMessage(), null, null, null, $started_at, $finished_at);
+ print_failure('Update action failed: ' . htmlspecialchars($e->getMessage()));
+ }
+ }
$_SESSION['gsp_update_csrf'] = gsp_random_token();
$csrf_token = $_SESSION['gsp_update_csrf'];
}
$current_version = gsp_get_current_version();
-$current_branch = gsp_get_current_branch();
-$git_commit = gsp_get_git_commit();
+ $current_branch = gsp_get_current_branch();
+ $git_commit = gsp_get_git_commit();
$last_layout = isset($_SESSION['gsp_last_update_layout']) && is_array($_SESSION['gsp_last_update_layout'])
? $_SESSION['gsp_last_update_layout']
: null;
-$vinfo = gsp_read_version_json();
-$latest_release = 'N/A';
-$backups = gsp_get_available_backups();
-$patch_overview = gsp_get_patch_overview();
+ $vinfo = gsp_read_version_json();
+ $latest_release = 'N/A';
+ $backup_base = gsp_get_backup_base($update_cfg);
+ $backup_retention = gsp_get_backup_retention($update_cfg);
+ $managed_backups = gsp_get_managed_backup_entries($backup_base);
+ $backups = gsp_get_available_backups($backup_base);
+ $patch_overview = gsp_get_patch_overview();
if ($apache_scan_result === null) {
$apache_scan_result = gsp_scan_apache_configs();
}
@@ -2570,7 +2737,9 @@ if ($git_commit) {
echo "
" . htmlspecialchars(GSP_UPDATE_LOG) . "" . htmlspecialchars($backup_base) . "SSL warning: " . intval($ssl_issue_count) . " Apache SSL certificate issue(s) detected. Updates are not blocked by this. See Advanced Diagnostics for details.
\n"; @@ -2595,9 +2764,10 @@ echo "git.git.