feat(steam_workshop): production rewrite with full admin profile page and server settings
- module.php: bump db_version to 2; full v2 schema in install_queries[0]; ALTER TABLE migration in install_queries[2] adding 11 new profile columns plus server_workshop_settings table
- WorkshopRepository: expand saveProfile() with all new fields (steam_app_id, steamcmd_login_mode, steamcmd_path, mod_separator, copy_keys, key paths, pre/post scripts, validation_notes); add nullOrStr helper; add server settings CRUD (getServerSettings, saveServerSettings, recordUpdateResult, setUpdateQueued, listQueuedUpdateHomes); update insertOrUpdateMod with custom_folder; update listAllEnabledMods to select new columns
- WorkshopProfileController: full extractProfileData() with all new fields; validateProfileData with folder template check
- views/admin/profile_form.php: full rewrite – all required fields (steam_app_id, workshop_app_id, login mode, SteamCMD path, OS, cache path, install path, folder naming dropdown, launch params, mod separator, copy method, copy keys with JS show/hide, key paths, pre/per-mod/post bash scripts with DayZ example, validation notes, config file template)
- views/admin/profiles.php: updated list view columns (App IDs, Login, Method)
- WorkshopInstaller.php: full rewrite with %var% template support (+ legacy {var} compat); buildTemplateVars() resolves all 14 variables; pre/post script execution; triggerSteamCmdDownload uses new profile fields; copyKeys helper
- WorkshopModController: add save_settings and queue_update POST actions; handleModsPage loads serverSettings + allProfiles
- views/user_workshop_mods.php: full rewrite – server settings form (enable, profile selector, update mode, restart behavior), update status grid (status/error/time/success time), queue update button, mod table with custom_folder column and toggle/order auto-submit
- lang/en_US.php: ~80 new string keys for all v2 fields
- steam_workshop.css: new v2 styles (3-col grid, script textarea, status grid, server settings card, badges)"
Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/e7f0d80d-f775-4794-adbd-cf48b55bc9c1
Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
parent
199b398543
commit
69f415ad86
10 changed files with 1334 additions and 312 deletions
|
|
@ -5,16 +5,22 @@ declare(strict_types=1);
|
|||
* WorkshopInstaller: handles mod download (via agent SteamCMD) and
|
||||
* copy/sync from agent cache to server install path.
|
||||
*
|
||||
* Template variables supported in all paths/scripts:
|
||||
* {home_id} numeric home id
|
||||
* {agent_id} numeric remote_server_id
|
||||
* {workshop_app_id} Steam app id (e.g. 221100)
|
||||
* {mod_id} Workshop mod id (numeric string)
|
||||
* {mod_title} mod title (sanitised)
|
||||
* {steamcmd_path} path to steamcmd.sh / steamcmd.exe on the agent
|
||||
* {server_path} game server home_path
|
||||
* {install_path} resolved install path for this mod
|
||||
* {cache_path} resolved cache path for this mod
|
||||
* Template variables supported in all paths/scripts (%var% style):
|
||||
* %home_id% numeric home id
|
||||
* %server_path% game server home_path
|
||||
* %steam_app_id% Steam game App ID (e.g. 221100 for DayZ)
|
||||
* %workshop_app_id% Workshop App ID used for +workshop_download_item
|
||||
* %workshop_id% Workshop mod item id (numeric)
|
||||
* %mod_name% mod title sanitised for use as a folder name
|
||||
* %install_name% resolved mod folder name (from folder_naming_format)
|
||||
* %download_path% alias for %source_path% (SteamCMD cache dir for this mod)
|
||||
* %source_path% SteamCMD cache directory for this mod
|
||||
* %target_path% resolved install directory for this mod
|
||||
* %keys_source_path% key source path (resolved from profile key_source_path)
|
||||
* %keys_target_path% key destination path (resolved from profile key_dest_path)
|
||||
* %steamcmd_path% path to steamcmd.sh on the agent
|
||||
*
|
||||
* Legacy {var} style placeholders are also resolved for backward compat.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/WorkshopRepository.php';
|
||||
|
|
@ -43,91 +49,82 @@ class WorkshopInstaller
|
|||
* @param array $home Row from getGameHome/getUserGameHome
|
||||
* @param array $profile Row from gsp_workshop_game_profiles
|
||||
* @param string $workshopId Numeric workshop item id
|
||||
* @param string $steamCmdPath Path to steamcmd binary on the agent
|
||||
* @return array{success:bool, message:string, restart_required:bool, log:list<string>}
|
||||
*/
|
||||
public function install(
|
||||
array $home,
|
||||
array $profile,
|
||||
string $workshopId,
|
||||
string $steamCmdPath = ''
|
||||
string $workshopId
|
||||
): array {
|
||||
$log = [];
|
||||
|
||||
// Validate workshop id
|
||||
$workshopId = preg_replace('/[^0-9]/', '', $workshopId) ?? '';
|
||||
if ($workshopId === '') {
|
||||
return $this->fail('Workshop ID must be numeric.', $log);
|
||||
}
|
||||
|
||||
$homeId = (int)($home['home_id'] ?? 0);
|
||||
$agentId = (int)($home['remote_server_id'] ?? 0);
|
||||
$appId = (string)($profile['workshop_app_id'] ?? '');
|
||||
$osType = $this->detectOsType($home);
|
||||
$homeId = (int)($home['home_id'] ?? 0);
|
||||
$agentId = (int)($home['remote_server_id'] ?? 0);
|
||||
$appId = (string)($profile['workshop_app_id'] ?? '');
|
||||
$osType = $this->detectOsType($home);
|
||||
|
||||
if ($homeId <= 0 || $agentId <= 0 || $appId === '') {
|
||||
return $this->fail('Invalid home, agent, or app ID.', $log);
|
||||
}
|
||||
|
||||
// Build template vars
|
||||
$vars = $this->buildTemplateVars($home, $profile, $workshopId, '', $steamCmdPath);
|
||||
$cachePath = $this->resolveTemplate((string)($profile['cache_path_template'] ?? ''), $vars);
|
||||
$installPath = $this->resolveTemplate((string)($profile['install_path_template'] ?? ''), $vars);
|
||||
$vars['{cache_path}'] = $cachePath;
|
||||
$vars['{install_path}'] = $installPath;
|
||||
|
||||
// Build remote library
|
||||
$remote = $this->buildRemote($home);
|
||||
if ($remote === null) {
|
||||
return $this->fail('Unable to connect to agent.', $log);
|
||||
}
|
||||
|
||||
// Check agent connectivity
|
||||
if ($remote->status_chk() !== 1) {
|
||||
return $this->fail('Agent is offline.', $log);
|
||||
}
|
||||
|
||||
// Check cache
|
||||
$cacheEntry = $this->repo->getCacheEntry($agentId, $appId, $workshopId);
|
||||
$log[] = "Cache check: agent={$agentId} app={$appId} mod={$workshopId}";
|
||||
// Build template vars (source/target paths filled after resolution below)
|
||||
$vars = $this->buildTemplateVars($home, $profile, $workshopId);
|
||||
|
||||
if ($cacheEntry === null || ($cacheEntry['status'] ?? '') !== 'cached') {
|
||||
$log[] = 'Cache MISS – triggering SteamCMD download on agent.';
|
||||
$downloadResult = $this->triggerSteamCmdDownload(
|
||||
$remote, $agentId, $appId, $workshopId, $steamCmdPath, $cachePath, $log
|
||||
);
|
||||
|
||||
if (!$downloadResult) {
|
||||
// Update cache status to 'missing' so the cron can retry
|
||||
$this->repo->upsertCacheEntry($agentId, $osType, $appId, $workshopId, $cachePath, 'missing');
|
||||
return $this->fail(
|
||||
'SteamCMD download failed. The mod will be retried on the next scheduled update.',
|
||||
$log
|
||||
);
|
||||
}
|
||||
|
||||
$log[] = 'SteamCMD download success.';
|
||||
$this->repo->upsertCacheEntry($agentId, $osType, $appId, $workshopId, $cachePath, 'cached');
|
||||
} else {
|
||||
$log[] = 'Cache HIT – using existing cached copy.';
|
||||
// Run pre-update script once (before mods)
|
||||
$preScript = trim((string)($profile['pre_update_script'] ?? ''));
|
||||
if ($preScript !== '') {
|
||||
$log[] = 'Running pre-update script.';
|
||||
$this->runScript($remote, $preScript, $vars, $log);
|
||||
}
|
||||
|
||||
// Copy / sync from cache to server install path
|
||||
// Download
|
||||
$cacheResult = $this->ensureCached($remote, $agentId, $osType, $appId, $workshopId, $profile, $vars, $log);
|
||||
if (!$cacheResult) {
|
||||
return $this->fail('SteamCMD download failed.', $log);
|
||||
}
|
||||
|
||||
// Copy/sync to server
|
||||
$syncResult = $this->syncToServer($remote, $profile, $vars, $log);
|
||||
if (!$syncResult) {
|
||||
return $this->fail('Sync from cache to server failed. Check agent logs.', $log);
|
||||
}
|
||||
|
||||
// Optional install script (admin-defined only)
|
||||
// Per-mod install script
|
||||
$installScript = trim((string)($profile['install_script'] ?? ''));
|
||||
if ($installScript !== '') {
|
||||
$this->runInstallScript($remote, $installScript, $vars, $log);
|
||||
$log[] = 'Running per-mod install script.';
|
||||
$this->runScript($remote, $installScript, $vars, $log);
|
||||
}
|
||||
|
||||
// Copy keys if configured
|
||||
if (!empty($profile['copy_keys'])) {
|
||||
$this->copyKeys($remote, $profile, $vars, $log);
|
||||
}
|
||||
|
||||
// Post-update script
|
||||
$postScript = trim((string)($profile['post_update_script'] ?? ''));
|
||||
if ($postScript !== '') {
|
||||
$log[] = 'Running post-update script.';
|
||||
$this->runScript($remote, $postScript, $vars, $log);
|
||||
}
|
||||
|
||||
// Record in database
|
||||
$this->repo->insertOrUpdateMod(
|
||||
$homeId, $agentId, (int)$profile['id'], $appId, $workshopId,
|
||||
$installPath, '', 0
|
||||
$vars['%target_path%'] ?? '', '', 0
|
||||
);
|
||||
|
||||
$restartRequired = !empty($profile['requires_restart']);
|
||||
|
|
@ -152,14 +149,13 @@ class WorkshopInstaller
|
|||
*/
|
||||
public function syncMod(array $home, array $modRow, array $profile): array
|
||||
{
|
||||
$log = [];
|
||||
$log = [];
|
||||
$workshopId = (string)($modRow['workshop_id'] ?? '');
|
||||
$agentId = (int)($modRow['agent_id'] ?? 0);
|
||||
$appId = (string)($modRow['workshop_app_id'] ?? '');
|
||||
|
||||
$cacheEntry = $this->repo->getCacheEntry($agentId, $appId, $workshopId);
|
||||
if ($cacheEntry === null || ($cacheEntry['status'] ?? '') !== 'cached') {
|
||||
$log[] = "Cache entry not available for mod {$workshopId} – skipping sync.";
|
||||
return ['success' => false, 'changed' => false, 'message' => 'Mod not cached yet.', 'log' => $log];
|
||||
}
|
||||
|
||||
|
|
@ -169,10 +165,8 @@ class WorkshopInstaller
|
|||
}
|
||||
|
||||
$vars = $this->buildTemplateVars($home, $profile, $workshopId, $modRow['title'] ?? '');
|
||||
$vars['{cache_path}'] = $this->resolveTemplate((string)($profile['cache_path_template'] ?? ''), $vars);
|
||||
$vars['{install_path}'] = (string)($modRow['install_path'] ?? $this->resolveTemplate((string)($profile['install_path_template'] ?? ''), $vars));
|
||||
|
||||
$changed = $this->checkNeedsSync($remote, $vars['{cache_path}'], $vars['{install_path}'], $profile, $log);
|
||||
$changed = $this->checkNeedsSync($remote, $vars['%source_path%'], $vars['%target_path%'], $profile, $log);
|
||||
if (!$changed) {
|
||||
$log[] = 'No changes detected – skipping sync.';
|
||||
return ['success' => true, 'changed' => false, 'message' => 'Already up to date.', 'log' => $log];
|
||||
|
|
@ -181,6 +175,16 @@ class WorkshopInstaller
|
|||
$log[] = 'Changes detected – syncing.';
|
||||
$ok = $this->syncToServer($remote, $profile, $vars, $log);
|
||||
|
||||
if ($ok) {
|
||||
$installScript = trim((string)($profile['install_script'] ?? ''));
|
||||
if ($installScript !== '') {
|
||||
$this->runScript($remote, $installScript, $vars, $log);
|
||||
}
|
||||
if (!empty($profile['copy_keys'])) {
|
||||
$this->copyKeys($remote, $profile, $vars, $log);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => $ok,
|
||||
'changed' => true,
|
||||
|
|
@ -190,17 +194,34 @@ class WorkshopInstaller
|
|||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Template resolution
|
||||
// Template resolution (public – used by WorkshopUpdater)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Replace template placeholders in a string.
|
||||
* Supports both %var% (canonical) and {var} (legacy) style.
|
||||
*
|
||||
* @param array<string,string> $vars
|
||||
*/
|
||||
public function resolveTemplate(string $template, array $vars): string
|
||||
{
|
||||
return str_replace(array_keys($vars), array_values($vars), $template);
|
||||
// %var% style (canonical)
|
||||
$result = str_replace(array_keys($vars), array_values($vars), $template);
|
||||
|
||||
// Legacy {var} style aliases – map old keys to same values
|
||||
$legacy = [];
|
||||
foreach ($vars as $k => $v) {
|
||||
$legacyKey = '{' . trim($k, '%') . '}';
|
||||
$legacy[$legacyKey] = $v;
|
||||
}
|
||||
// Extra legacy aliases
|
||||
$legacy['{mod_id}'] = $vars['%workshop_id%'] ?? '';
|
||||
$legacy['{mod_title}'] = $vars['%mod_name%'] ?? '';
|
||||
$legacy['{mod_folder}'] = $vars['%install_name%'] ?? '';
|
||||
$legacy['{install_path}'] = $vars['%target_path%'] ?? '';
|
||||
$legacy['{cache_path}'] = $vars['%source_path%'] ?? '';
|
||||
|
||||
return str_replace(array_keys($legacy), array_values($legacy), $result);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -212,30 +233,67 @@ class WorkshopInstaller
|
|||
array $home,
|
||||
array $profile,
|
||||
string $workshopId,
|
||||
string $modTitle = '',
|
||||
string $steamCmdPath = ''
|
||||
string $modTitle = ''
|
||||
): array {
|
||||
$serverPath = rtrim((string)($home['home_path'] ?? ''), '/');
|
||||
$safeName = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $modTitle) ?? '';
|
||||
$serverPath = rtrim((string)($home['home_path'] ?? ''), '/');
|
||||
$steamcmdPath = trim((string)($profile['steamcmd_path'] ?? ''));
|
||||
if ($steamcmdPath === '') {
|
||||
$steamcmdPath = '/home/gameserver/steamcmd/steamcmd.sh';
|
||||
}
|
||||
|
||||
$folderNameTpl = (string)($profile['folder_name_template'] ?? '@{mod_id}');
|
||||
$folderNameVars = [
|
||||
'{mod_id}' => $workshopId,
|
||||
'{mod_title}' => $safeName,
|
||||
];
|
||||
$folderName = str_replace(array_keys($folderNameVars), array_values($folderNameVars), $folderNameTpl);
|
||||
$safeName = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $modTitle) ?? '';
|
||||
|
||||
// Resolve folder name from format
|
||||
$folderFormat = (string)($profile['folder_naming_format'] ?? '@%workshop_id%');
|
||||
if ($folderFormat === '@%mod_name%') {
|
||||
$installName = '@' . $safeName;
|
||||
} elseif ($folderFormat === '@%workshop_id%') {
|
||||
$installName = '@' . $workshopId;
|
||||
} else {
|
||||
// custom – use folder_name_template as-is, resolve %workshop_id%/%mod_name% inline
|
||||
$tpl = (string)($profile['folder_name_template'] ?? '@%workshop_id%');
|
||||
$installName = str_replace(['%workshop_id%', '%mod_name%'], [$workshopId, $safeName], $tpl);
|
||||
}
|
||||
|
||||
$steamAppId = (string)($profile['steam_app_id'] ?? '');
|
||||
$workshopAppId = (string)($profile['workshop_app_id'] ?? '');
|
||||
|
||||
// Resolve cache/source path template
|
||||
$cachePathTpl = (string)($profile['cache_path_template'] ?? '');
|
||||
$sourcePath = str_replace(
|
||||
['%workshop_app_id%', '%workshop_id%', '%mod_name%', '%install_name%', '%steam_app_id%', '%steamcmd_path%'],
|
||||
[$workshopAppId, $workshopId, $safeName, $installName, $steamAppId, dirname($steamcmdPath)],
|
||||
$cachePathTpl
|
||||
);
|
||||
|
||||
// Resolve target/install path template
|
||||
$installPathTpl = (string)($profile['install_path_template'] ?? '');
|
||||
$targetPath = str_replace(
|
||||
['%server_path%', '%workshop_app_id%', '%workshop_id%', '%mod_name%', '%install_name%', '%steam_app_id%'],
|
||||
[$serverPath, $workshopAppId, $workshopId, $safeName, $installName, $steamAppId],
|
||||
$installPathTpl
|
||||
);
|
||||
|
||||
// Resolve key paths
|
||||
$keySourceRaw = (string)($profile['key_source_path'] ?? '');
|
||||
$keyDestRaw = (string)($profile['key_dest_path'] ?? '');
|
||||
$keySource = str_replace(['%source_path%', '%server_path%'], [$sourcePath, $serverPath], $keySourceRaw);
|
||||
$keyDest = str_replace(['%target_path%', '%server_path%'], [$targetPath, $serverPath], $keyDestRaw);
|
||||
|
||||
return [
|
||||
'{home_id}' => (string)($home['home_id'] ?? ''),
|
||||
'{agent_id}' => (string)($home['remote_server_id'] ?? ''),
|
||||
'{workshop_app_id}' => (string)($profile['workshop_app_id'] ?? ''),
|
||||
'{mod_id}' => $workshopId,
|
||||
'{mod_title}' => $safeName,
|
||||
'{mod_folder}' => $folderName,
|
||||
'{steamcmd_path}' => $steamCmdPath !== '' ? $steamCmdPath : '/home/gameserver/steamcmd',
|
||||
'{server_path}' => $serverPath,
|
||||
'{install_path}' => '', // filled by caller after resolution
|
||||
'{cache_path}' => '', // filled by caller after resolution
|
||||
'%home_id%' => (string)($home['home_id'] ?? ''),
|
||||
'%server_path%' => $serverPath,
|
||||
'%steam_app_id%' => $steamAppId,
|
||||
'%workshop_app_id%' => $workshopAppId,
|
||||
'%workshop_id%' => $workshopId,
|
||||
'%mod_name%' => $safeName,
|
||||
'%install_name%' => $installName,
|
||||
'%download_path%' => $sourcePath,
|
||||
'%source_path%' => $sourcePath,
|
||||
'%target_path%' => $targetPath,
|
||||
'%keys_source_path%' => $keySource,
|
||||
'%keys_target_path%' => $keyDest,
|
||||
'%steamcmd_path%' => $steamcmdPath,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -243,6 +301,44 @@ class WorkshopInstaller
|
|||
// Private helpers
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Ensure a mod is downloaded/cached on the agent.
|
||||
* Returns true if cached and available.
|
||||
*
|
||||
* @param list<string> $log
|
||||
*/
|
||||
private function ensureCached(
|
||||
object $remote,
|
||||
int $agentId,
|
||||
string $osType,
|
||||
string $appId,
|
||||
string $workshopId,
|
||||
array $profile,
|
||||
array &$vars,
|
||||
array &$log
|
||||
): bool {
|
||||
$sourcePath = $vars['%source_path%'];
|
||||
|
||||
$cacheEntry = $this->repo->getCacheEntry($agentId, $appId, $workshopId);
|
||||
$log[] = "Cache check: agent={$agentId} app={$appId} mod={$workshopId}";
|
||||
|
||||
if ($cacheEntry !== null && ($cacheEntry['status'] ?? '') === 'cached') {
|
||||
$log[] = 'Cache HIT – using existing cached copy.';
|
||||
return true;
|
||||
}
|
||||
|
||||
$log[] = 'Cache MISS – triggering SteamCMD download on agent.';
|
||||
$ok = $this->triggerSteamCmdDownload($remote, $agentId, $appId, $workshopId, $profile, $sourcePath, $log);
|
||||
|
||||
$status = $ok ? 'cached' : 'missing';
|
||||
$this->repo->upsertCacheEntry($agentId, $osType, $appId, $workshopId, $sourcePath, $status);
|
||||
|
||||
if ($ok) {
|
||||
$log[] = 'SteamCMD download success.';
|
||||
}
|
||||
return $ok;
|
||||
}
|
||||
|
||||
/** Build an OGPRemoteLibrary instance from a home row. */
|
||||
private function buildRemote(array $home): ?object
|
||||
{
|
||||
|
|
@ -253,9 +349,9 @@ class WorkshopInstaller
|
|||
return null;
|
||||
}
|
||||
|
||||
$ip = (string)($home['agent_ip'] ?? '');
|
||||
$port = (string)($home['agent_port'] ?? '');
|
||||
$key = (string)($home['encryption_key'] ?? '');
|
||||
$ip = (string)($home['agent_ip'] ?? '');
|
||||
$port = (string)($home['agent_port'] ?? '');
|
||||
$key = (string)($home['encryption_key'] ?? '');
|
||||
$timeout = isset($home['timeout']) ? (int)$home['timeout'] : 30;
|
||||
|
||||
if ($ip === '' || $port === '') {
|
||||
|
|
@ -266,7 +362,7 @@ class WorkshopInstaller
|
|||
}
|
||||
|
||||
/**
|
||||
* Trigger a SteamCMD workshop_download_item on the agent via exec().
|
||||
* Trigger a SteamCMD workshop_download_item on the agent.
|
||||
* Returns true on success.
|
||||
*
|
||||
* @param list<string> $log
|
||||
|
|
@ -276,36 +372,38 @@ class WorkshopInstaller
|
|||
int $agentId,
|
||||
string $appId,
|
||||
string $workshopId,
|
||||
string $steamCmdPath,
|
||||
array $profile,
|
||||
string $cachePath,
|
||||
array &$log
|
||||
): bool {
|
||||
if ($steamCmdPath === '') {
|
||||
$steamCmdPath = '/home/gameserver/steamcmd/steamcmd.sh';
|
||||
$steamcmdPath = trim((string)($profile['steamcmd_path'] ?? ''));
|
||||
if ($steamcmdPath === '') {
|
||||
$steamcmdPath = '/home/gameserver/steamcmd/steamcmd.sh';
|
||||
}
|
||||
|
||||
$loginMode = (string)($profile['steamcmd_login_mode'] ?? 'anonymous');
|
||||
$loginArg = $loginMode === 'account' ? 'account_placeholder' : 'anonymous';
|
||||
|
||||
$cmd = implode(' ', [
|
||||
escapeshellarg($steamCmdPath),
|
||||
'+login', 'anonymous',
|
||||
escapeshellarg($steamcmdPath),
|
||||
'+login', escapeshellarg($loginArg),
|
||||
'+workshop_download_item', escapeshellarg($appId), escapeshellarg($workshopId),
|
||||
'validate',
|
||||
'+quit',
|
||||
]);
|
||||
|
||||
$log[] = "SteamCMD start: {$cmd}";
|
||||
$log[] = "SteamCMD start: agent={$agentId} app={$appId} mod={$workshopId}";
|
||||
$this->writeLog("STEAMCMD START agent={$agentId} app={$appId} mod={$workshopId}");
|
||||
|
||||
$output = $remote->exec($cmd);
|
||||
|
||||
if ($output === null) {
|
||||
$log[] = 'SteamCMD: no response from agent (command may still be running).';
|
||||
$this->writeLog("STEAMCMD NO_RESPONSE agent={$agentId} app={$appId} mod={$workshopId}");
|
||||
// Treat as unknown – check file existence
|
||||
} else {
|
||||
$log[] = 'SteamCMD output: ' . substr((string)$output, 0, 500);
|
||||
}
|
||||
|
||||
// Verify the download succeeded by checking for the cache path on the agent
|
||||
// Verify by checking whether the cache path now exists
|
||||
$exists = $remote->rfile_exists($cachePath);
|
||||
if ($exists === 1) {
|
||||
$this->writeLog("STEAMCMD SUCCESS agent={$agentId} app={$appId} mod={$workshopId} path={$cachePath}");
|
||||
|
|
@ -317,7 +415,7 @@ class WorkshopInstaller
|
|||
}
|
||||
|
||||
/**
|
||||
* Check if cache path differs from install path using a dry-run compare.
|
||||
* Check if cache path differs from install path (dry-run compare).
|
||||
* Returns true if sync is needed.
|
||||
*
|
||||
* @param list<string> $log
|
||||
|
|
@ -333,36 +431,17 @@ class WorkshopInstaller
|
|||
$log[] = "Pre-start compare: cache={$cachePath} dest={$installPath} method={$copyMethod}";
|
||||
|
||||
if ($copyMethod === 'rsync') {
|
||||
// Dry-run: any output lines (beyond the exit sentinel) mean changes exist
|
||||
$cmd = sprintf(
|
||||
$cmd = sprintf(
|
||||
'rsync -rcn --delete %s %s 2>/dev/null; echo "RSYNC_EXIT:$?"',
|
||||
escapeshellarg(rtrim($cachePath, '/') . '/'),
|
||||
escapeshellarg(rtrim($installPath, '/') . '/')
|
||||
);
|
||||
$out = (string)$remote->exec($cmd);
|
||||
// Strip the exit line, then check for any non-whitespace output
|
||||
$body = preg_replace('/RSYNC_EXIT:\d+\s*$/', '', $out) ?? '';
|
||||
return preg_match('/\S/', $body) === 1;
|
||||
}
|
||||
|
||||
if ($copyMethod === 'robocopy') {
|
||||
// List-only mode: robocopy exit code 0 = no differences, 1+ = changes or errors.
|
||||
// Embed the exit code in output so we can read it back via exec().
|
||||
$cmd = sprintf(
|
||||
'robocopy /L /MIR /NJH /NJS %s %s; echo "ROBOCOPY_EXIT:$LASTEXITCODE"',
|
||||
escapeshellarg($cachePath),
|
||||
escapeshellarg($installPath)
|
||||
);
|
||||
$out = (string)$remote->exec($cmd);
|
||||
if (preg_match('/ROBOCOPY_EXIT:(\d+)/', $out, $m)) {
|
||||
// 0 = no change; 1–7 = informational (changes found); 8+ = error
|
||||
return (int)$m[1] !== 0;
|
||||
}
|
||||
// If we cannot determine, assume sync is needed
|
||||
return true;
|
||||
}
|
||||
|
||||
// custom_script: always sync
|
||||
// copy / symlink: always sync
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -375,98 +454,110 @@ class WorkshopInstaller
|
|||
private function syncToServer(
|
||||
object $remote,
|
||||
array $profile,
|
||||
array $vars,
|
||||
array &$vars,
|
||||
array &$log
|
||||
): bool {
|
||||
$copyMethod = (string)($profile['copy_method'] ?? 'rsync');
|
||||
$cachePath = $vars['{cache_path}'] ?? '';
|
||||
$installPath = $vars['{install_path}'] ?? '';
|
||||
$sourcePath = $vars['%source_path%'];
|
||||
$targetPath = $vars['%target_path%'];
|
||||
|
||||
if ($cachePath === '' || $installPath === '') {
|
||||
$log[] = 'Sync skipped: empty cache or install path.';
|
||||
if ($sourcePath === '' || $targetPath === '') {
|
||||
$log[] = 'Sync skipped: empty source or target path.';
|
||||
return false;
|
||||
}
|
||||
|
||||
$log[] = "Sync start: method={$copyMethod} cache={$cachePath} dest={$installPath}";
|
||||
$this->writeLog("COPY START method={$copyMethod} cache={$cachePath} dest={$installPath}");
|
||||
$log[] = "Sync start: method={$copyMethod} source={$sourcePath} target={$targetPath}";
|
||||
$this->writeLog("COPY START method={$copyMethod} source={$sourcePath} target={$targetPath}");
|
||||
|
||||
if ($copyMethod === 'rsync') {
|
||||
$cmd = sprintf(
|
||||
'mkdir -p %s && rsync -a --delete %s %s 2>&1; echo "EXIT:$?"',
|
||||
escapeshellarg($installPath),
|
||||
escapeshellarg(rtrim($cachePath, '/') . '/'),
|
||||
escapeshellarg(rtrim($installPath, '/') . '/')
|
||||
escapeshellarg($targetPath),
|
||||
escapeshellarg(rtrim($sourcePath, '/') . '/'),
|
||||
escapeshellarg(rtrim($targetPath, '/') . '/')
|
||||
);
|
||||
} elseif ($copyMethod === 'robocopy') {
|
||||
} elseif ($copyMethod === 'symlink') {
|
||||
$cmd = sprintf(
|
||||
'robocopy /MIR /NJH /NJS %s %s; echo "ROBOCOPY_EXIT:$LASTEXITCODE"',
|
||||
escapeshellarg($cachePath),
|
||||
escapeshellarg($installPath)
|
||||
'mkdir -p %s && ln -sfn %s %s 2>&1; echo "EXIT:$?"',
|
||||
escapeshellarg(dirname($targetPath)),
|
||||
escapeshellarg($sourcePath),
|
||||
escapeshellarg($targetPath)
|
||||
);
|
||||
} elseif ($copyMethod === 'custom_script') {
|
||||
$script = trim((string)($profile['install_script'] ?? ''));
|
||||
if ($script === '') {
|
||||
$log[] = 'custom_script requested but install_script is empty – falling back to rsync.';
|
||||
$cmd = sprintf(
|
||||
'mkdir -p %s && rsync -a --delete %s %s 2>&1; echo "EXIT:$?"',
|
||||
escapeshellarg($installPath),
|
||||
escapeshellarg(rtrim($cachePath, '/') . '/'),
|
||||
escapeshellarg(rtrim($installPath, '/') . '/')
|
||||
);
|
||||
} else {
|
||||
// The admin-defined script is templated; execute it via the agent exec()
|
||||
$resolvedScript = $this->resolveTemplate($script, $vars);
|
||||
$cmd = $resolvedScript . ' 2>&1; echo "EXIT:$?"';
|
||||
}
|
||||
} else {
|
||||
$log[] = "Unknown copy method '{$copyMethod}'.";
|
||||
return false;
|
||||
// 'copy' – basic cp
|
||||
$cmd = sprintf(
|
||||
'mkdir -p %s && cp -r %s %s 2>&1; echo "EXIT:$?"',
|
||||
escapeshellarg($targetPath),
|
||||
escapeshellarg(rtrim($sourcePath, '/') . '/.'),
|
||||
escapeshellarg($targetPath)
|
||||
);
|
||||
}
|
||||
|
||||
$out = (string)$remote->exec($cmd);
|
||||
$log[] = 'Sync output: ' . substr($out, 0, 500);
|
||||
|
||||
// Determine success from embedded exit code sentinel
|
||||
if ($copyMethod === 'robocopy') {
|
||||
if (preg_match('/ROBOCOPY_EXIT:(\d+)/', $out, $m)) {
|
||||
// 0–7 = success/informational; 8+ = error
|
||||
$ok = (int)$m[1] < 8;
|
||||
} else {
|
||||
$ok = true; // assume success if no code extracted
|
||||
}
|
||||
if (preg_match('/EXIT:(\d+)/', $out, $m)) {
|
||||
$ok = (int)$m[1] === 0;
|
||||
} else {
|
||||
if (preg_match('/EXIT:(\d+)/', $out, $m)) {
|
||||
$ok = (int)$m[1] === 0;
|
||||
} else {
|
||||
$ok = true;
|
||||
}
|
||||
$ok = true;
|
||||
}
|
||||
|
||||
if ($ok) {
|
||||
$log[] = 'Sync success.';
|
||||
$this->writeLog("COPY SUCCESS cache={$cachePath} dest={$installPath}");
|
||||
$this->writeLog("COPY SUCCESS source={$sourcePath} target={$targetPath}");
|
||||
} else {
|
||||
$log[] = 'Sync failed (non-zero exit).';
|
||||
$this->writeLog("COPY FAILURE cache={$cachePath} dest={$installPath}");
|
||||
$this->writeLog("COPY FAILURE source={$sourcePath} target={$targetPath}");
|
||||
}
|
||||
|
||||
return $ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the admin-defined install script on the agent.
|
||||
* Copy key files from the mod's keys directory to the server keys directory.
|
||||
*
|
||||
* @param array<string,string> $vars
|
||||
* @param list<string> $log
|
||||
*/
|
||||
private function runInstallScript(
|
||||
private function copyKeys(
|
||||
object $remote,
|
||||
array $profile,
|
||||
array $vars,
|
||||
array &$log
|
||||
): void {
|
||||
$keySrc = $vars['%keys_source_path%'];
|
||||
$keyDest = $vars['%keys_target_path%'];
|
||||
|
||||
if ($keySrc === '' || $keyDest === '') {
|
||||
$log[] = 'Key copy skipped: key paths not configured.';
|
||||
return;
|
||||
}
|
||||
|
||||
$log[] = "Copying keys: {$keySrc} → {$keyDest}";
|
||||
$cmd = sprintf(
|
||||
'if [ -d %s ]; then mkdir -p %s && cp -f %s/*.bikey %s/ 2>/dev/null; fi; echo "EXIT:$?"',
|
||||
escapeshellarg($keySrc),
|
||||
escapeshellarg($keyDest),
|
||||
escapeshellarg($keySrc),
|
||||
escapeshellarg($keyDest)
|
||||
);
|
||||
$out = (string)$remote->exec($cmd);
|
||||
$log[] = 'Key copy output: ' . substr($out, 0, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an admin-defined bash script on the agent after resolving template vars.
|
||||
*
|
||||
* @param array<string,string> $vars
|
||||
* @param list<string> $log
|
||||
*/
|
||||
private function runScript(
|
||||
object $remote,
|
||||
string $script,
|
||||
array $vars,
|
||||
array &$log
|
||||
): void {
|
||||
$resolved = $this->resolveTemplate($script, $vars);
|
||||
$log[] = 'Running install script.';
|
||||
$out = (string)$remote->exec($resolved . ' 2>&1');
|
||||
$log[] = 'Script output: ' . substr($out, 0, 500);
|
||||
$this->writeLog('SCRIPT OUTPUT: ' . substr($out, 0, 1000));
|
||||
|
|
@ -475,10 +566,7 @@ class WorkshopInstaller
|
|||
private function detectOsType(array $home): string
|
||||
{
|
||||
$gameKey = strtolower((string)($home['game_key'] ?? ''));
|
||||
if (preg_match('/win/', $gameKey)) {
|
||||
return 'windows';
|
||||
}
|
||||
return 'linux';
|
||||
return preg_match('/win/', $gameKey) ? 'windows' : 'linux';
|
||||
}
|
||||
|
||||
private function writeLog(string $message): void
|
||||
|
|
|
|||
|
|
@ -97,35 +97,69 @@ class WorkshopRepository
|
|||
|
||||
$gameKey = $this->esc($data['game_key'] ?? '');
|
||||
$gameName = $this->esc($data['game_name'] ?? '');
|
||||
$steamAppId = $this->esc($data['steam_app_id'] ?? '');
|
||||
$workshopAppId = $this->esc($data['workshop_app_id'] ?? '');
|
||||
$steamLoginRequired = empty($data['steam_login_required']) ? 0 : 1;
|
||||
$steamcmdLoginMode = in_array($data['steamcmd_login_mode'] ?? '', ['anonymous', 'account'], true)
|
||||
? $this->esc($data['steamcmd_login_mode'])
|
||||
: 'anonymous';
|
||||
$steamcmdPath = $this->esc($data['steamcmd_path'] ?? '');
|
||||
$supportedOs = $this->esc($data['supported_os'] ?? 'linux');
|
||||
$cachePathTpl = $this->esc($data['cache_path_template'] ?? '');
|
||||
$installPathTpl = $this->esc($data['install_path_template'] ?? '');
|
||||
$folderNameTpl = $this->esc($data['folder_name_template'] ?? '@{mod_id}');
|
||||
$copyMethod = $this->esc($data['copy_method'] ?? 'rsync');
|
||||
$installScript = isset($data['install_script']) && $data['install_script'] !== '' ? "'" . $this->esc($data['install_script']) . "'" : 'NULL';
|
||||
$configFileTpl = isset($data['config_file_template']) && $data['config_file_template'] !== '' ? "'" . $this->esc($data['config_file_template']) . "'" : 'NULL';
|
||||
$launchParamTpl = isset($data['launch_param_template']) && $data['launch_param_template'] !== '' ? "'" . $this->esc($data['launch_param_template']) . "'" : 'NULL';
|
||||
$folderNamingFormat = in_array($data['folder_naming_format'] ?? '', ['@%mod_name%', '@%workshop_id%', 'custom'], true)
|
||||
? $this->esc($data['folder_naming_format'])
|
||||
: '@%workshop_id%';
|
||||
$folderNameTpl = $this->esc($data['folder_name_template'] ?? '@%workshop_id%');
|
||||
$modLaunchParam = $this->esc($data['mod_launch_param'] ?? '');
|
||||
$modSeparator = in_array($data['mod_separator'] ?? '', ['semicolon', 'comma', 'space'], true)
|
||||
? $this->esc($data['mod_separator'])
|
||||
: 'semicolon';
|
||||
$copyMethod = in_array($data['copy_method'] ?? '', ['copy', 'rsync', 'symlink'], true)
|
||||
? $this->esc($data['copy_method'])
|
||||
: 'rsync';
|
||||
$copyKeys = empty($data['copy_keys']) ? 0 : 1;
|
||||
$keySourcePath = $this->nullOrStr($data['key_source_path'] ?? '');
|
||||
$keyDestPath = $this->nullOrStr($data['key_dest_path'] ?? '');
|
||||
$preUpdateScript = $this->nullOrStr($data['pre_update_script'] ?? '');
|
||||
$installScript = $this->nullOrStr($data['install_script'] ?? '');
|
||||
$postUpdateScript = $this->nullOrStr($data['post_update_script'] ?? '');
|
||||
$configFileTpl = $this->nullOrStr($data['config_file_template'] ?? '');
|
||||
$launchParamTpl = $this->nullOrStr($data['launch_param_template'] ?? '');
|
||||
$requiresRestart = empty($data['requires_restart']) ? 0 : 1;
|
||||
$validationNotes = $this->nullOrStr($data['validation_notes'] ?? '');
|
||||
$enabled = isset($data['enabled']) && !$data['enabled'] ? 0 : 1;
|
||||
|
||||
if ($id > 0) {
|
||||
$this->exec(
|
||||
"UPDATE `{$this->prefix}workshop_game_profiles` SET
|
||||
game_key = '{$gameKey}',
|
||||
game_name = '{$gameName}',
|
||||
workshop_app_id = '{$workshopAppId}',
|
||||
supported_os = '{$supportedOs}',
|
||||
cache_path_template = '{$cachePathTpl}',
|
||||
game_key = '{$gameKey}',
|
||||
game_name = '{$gameName}',
|
||||
steam_app_id = '{$steamAppId}',
|
||||
workshop_app_id = '{$workshopAppId}',
|
||||
steam_login_required = {$steamLoginRequired},
|
||||
steamcmd_login_mode = '{$steamcmdLoginMode}',
|
||||
steamcmd_path = '{$steamcmdPath}',
|
||||
supported_os = '{$supportedOs}',
|
||||
cache_path_template = '{$cachePathTpl}',
|
||||
install_path_template = '{$installPathTpl}',
|
||||
folder_name_template = '{$folderNameTpl}',
|
||||
copy_method = '{$copyMethod}',
|
||||
install_script = {$installScript},
|
||||
config_file_template = {$configFileTpl},
|
||||
folder_naming_format = '{$folderNamingFormat}',
|
||||
folder_name_template = '{$folderNameTpl}',
|
||||
mod_launch_param = '{$modLaunchParam}',
|
||||
mod_separator = '{$modSeparator}',
|
||||
copy_method = '{$copyMethod}',
|
||||
copy_keys = {$copyKeys},
|
||||
key_source_path = {$keySourcePath},
|
||||
key_dest_path = {$keyDestPath},
|
||||
pre_update_script = {$preUpdateScript},
|
||||
install_script = {$installScript},
|
||||
post_update_script = {$postUpdateScript},
|
||||
config_file_template = {$configFileTpl},
|
||||
launch_param_template = {$launchParamTpl},
|
||||
requires_restart = {$requiresRestart},
|
||||
enabled = {$enabled},
|
||||
updated_at = NOW()
|
||||
requires_restart = {$requiresRestart},
|
||||
validation_notes = {$validationNotes},
|
||||
enabled = {$enabled},
|
||||
updated_at = NOW()
|
||||
WHERE id = {$id}"
|
||||
);
|
||||
return $id;
|
||||
|
|
@ -133,17 +167,31 @@ class WorkshopRepository
|
|||
|
||||
$this->exec(
|
||||
"INSERT INTO `{$this->prefix}workshop_game_profiles`
|
||||
(game_key, game_name, workshop_app_id, supported_os, cache_path_template,
|
||||
install_path_template, folder_name_template, copy_method, install_script,
|
||||
config_file_template, launch_param_template, requires_restart, enabled, created_at)
|
||||
(game_key, game_name, steam_app_id, workshop_app_id, steam_login_required,
|
||||
steamcmd_login_mode, steamcmd_path, supported_os, cache_path_template,
|
||||
install_path_template, folder_naming_format, folder_name_template,
|
||||
mod_launch_param, mod_separator, copy_method, copy_keys,
|
||||
key_source_path, key_dest_path, pre_update_script, install_script,
|
||||
post_update_script, config_file_template, launch_param_template,
|
||||
requires_restart, validation_notes, enabled, created_at)
|
||||
VALUES
|
||||
('{$gameKey}', '{$gameName}', '{$workshopAppId}', '{$supportedOs}', '{$cachePathTpl}',
|
||||
'{$installPathTpl}', '{$folderNameTpl}', '{$copyMethod}', {$installScript},
|
||||
{$configFileTpl}, {$launchParamTpl}, {$requiresRestart}, {$enabled}, NOW())"
|
||||
('{$gameKey}', '{$gameName}', '{$steamAppId}', '{$workshopAppId}', {$steamLoginRequired},
|
||||
'{$steamcmdLoginMode}', '{$steamcmdPath}', '{$supportedOs}', '{$cachePathTpl}',
|
||||
'{$installPathTpl}', '{$folderNamingFormat}', '{$folderNameTpl}',
|
||||
'{$modLaunchParam}', '{$modSeparator}', '{$copyMethod}', {$copyKeys},
|
||||
{$keySourcePath}, {$keyDestPath}, {$preUpdateScript}, {$installScript},
|
||||
{$postUpdateScript}, {$configFileTpl}, {$launchParamTpl},
|
||||
{$requiresRestart}, {$validationNotes}, {$enabled}, NOW())"
|
||||
);
|
||||
return $this->lastInsertId();
|
||||
}
|
||||
|
||||
/** Return NULL or an escaped quoted string, for optional TEXT columns. */
|
||||
private function nullOrStr(string $value): string
|
||||
{
|
||||
return $value !== '' ? "'" . $this->esc($value) . "'" : 'NULL';
|
||||
}
|
||||
|
||||
public function deleteProfile(int $id): bool
|
||||
{
|
||||
return $this->exec(
|
||||
|
|
@ -270,6 +318,10 @@ class WorkshopRepository
|
|||
* Insert a new mod row or update the existing one (upsert by home_id + workshop_id).
|
||||
* Returns the row id.
|
||||
*/
|
||||
/**
|
||||
* Insert (id = 0) or update (id > 0) a Workshop mod entry for a game home.
|
||||
* Returns the row id.
|
||||
*/
|
||||
public function insertOrUpdateMod(
|
||||
int $homeId,
|
||||
int $agentId,
|
||||
|
|
@ -278,12 +330,14 @@ class WorkshopRepository
|
|||
string $workshopId,
|
||||
string $installPath,
|
||||
string $title = '',
|
||||
int $loadOrder = 0
|
||||
int $loadOrder = 0,
|
||||
string $customFolder = ''
|
||||
): int {
|
||||
$appId = $this->esc($appId);
|
||||
$workshopId = $this->esc($workshopId);
|
||||
$installPath = $this->esc($installPath);
|
||||
$title = $this->esc($title);
|
||||
$appId = $this->esc($appId);
|
||||
$workshopId = $this->esc($workshopId);
|
||||
$installPath = $this->esc($installPath);
|
||||
$title = $this->esc($title);
|
||||
$customFolder = $this->esc($customFolder);
|
||||
|
||||
$existing = $this->getServerMod($homeId, $workshopId);
|
||||
|
||||
|
|
@ -294,6 +348,7 @@ class WorkshopRepository
|
|||
profile_id = {$profileId},
|
||||
workshop_app_id = '{$appId}',
|
||||
title = '{$title}',
|
||||
custom_folder = '{$customFolder}',
|
||||
install_path = '{$installPath}',
|
||||
load_order = {$loadOrder},
|
||||
enabled = 1,
|
||||
|
|
@ -305,9 +360,9 @@ class WorkshopRepository
|
|||
|
||||
$this->exec(
|
||||
"INSERT INTO `{$this->prefix}server_workshop_mods`
|
||||
(home_id, agent_id, profile_id, workshop_app_id, workshop_id, title, enabled, install_path, load_order, installed_at)
|
||||
(home_id, agent_id, profile_id, workshop_app_id, workshop_id, title, custom_folder, enabled, install_path, load_order, installed_at)
|
||||
VALUES
|
||||
({$homeId}, {$agentId}, {$profileId}, '{$appId}', '{$workshopId}', '{$title}', 1, '{$installPath}', {$loadOrder}, NOW())"
|
||||
({$homeId}, {$agentId}, {$profileId}, '{$appId}', '{$workshopId}', '{$title}', '{$customFolder}', 1, '{$installPath}', {$loadOrder}, NOW())"
|
||||
);
|
||||
return $this->lastInsertId();
|
||||
}
|
||||
|
|
@ -349,9 +404,13 @@ class WorkshopRepository
|
|||
{
|
||||
return $this->select(
|
||||
"SELECT m.*,
|
||||
p.cache_path_template, p.install_path_template, p.folder_name_template,
|
||||
p.copy_method, p.install_script, p.config_file_template, p.launch_param_template,
|
||||
p.requires_restart
|
||||
p.steam_app_id, p.cache_path_template, p.install_path_template,
|
||||
p.folder_naming_format, p.folder_name_template,
|
||||
p.copy_method, p.copy_keys, p.key_source_path, p.key_dest_path,
|
||||
p.pre_update_script, p.install_script, p.post_update_script,
|
||||
p.steamcmd_path, p.steamcmd_login_mode,
|
||||
p.config_file_template, p.launch_param_template,
|
||||
p.requires_restart
|
||||
FROM `{$this->prefix}server_workshop_mods` m
|
||||
JOIN `{$this->prefix}workshop_game_profiles` p ON m.profile_id = p.id
|
||||
WHERE m.enabled = 1 AND p.enabled = 1
|
||||
|
|
@ -435,4 +494,100 @@ class WorkshopRepository
|
|||
WHERE m.enabled = 1 AND m.profile_id = {$profileId}"
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// SERVER WORKSHOP SETTINGS (per-server/home configuration)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return the workshop settings row for a game home, or null if not set.
|
||||
*/
|
||||
public function getServerSettings(int $homeId): ?array
|
||||
{
|
||||
return $this->selectOne(
|
||||
"SELECT * FROM `{$this->prefix}server_workshop_settings`
|
||||
WHERE home_id = {$homeId} LIMIT 1"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert server-level workshop settings.
|
||||
*/
|
||||
public function saveServerSettings(int $homeId, array $data): void
|
||||
{
|
||||
$workshopEnabled = empty($data['workshop_enabled']) ? 0 : 1;
|
||||
$profileId = isset($data['profile_id']) && (int)$data['profile_id'] > 0
|
||||
? (int)$data['profile_id']
|
||||
: 'NULL';
|
||||
$updateMode = in_array($data['update_mode'] ?? '', ['manual', 'scheduled', 'on_restart'], true)
|
||||
? "'" . $this->esc($data['update_mode']) . "'"
|
||||
: "'manual'";
|
||||
$restartBehavior = in_array($data['restart_behavior'] ?? '', ['none', 'queue', 'stop_update_start'], true)
|
||||
? "'" . $this->esc($data['restart_behavior']) . "'"
|
||||
: "'none'";
|
||||
$updateQueued = empty($data['update_queued']) ? 0 : 1;
|
||||
|
||||
$this->exec(
|
||||
"INSERT INTO `{$this->prefix}server_workshop_settings`
|
||||
(home_id, workshop_enabled, profile_id, update_mode, restart_behavior, update_queued, updated_at)
|
||||
VALUES
|
||||
({$homeId}, {$workshopEnabled}, {$profileId}, {$updateMode}, {$restartBehavior}, {$updateQueued}, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
workshop_enabled = {$workshopEnabled},
|
||||
profile_id = {$profileId},
|
||||
update_mode = {$updateMode},
|
||||
restart_behavior = {$restartBehavior},
|
||||
update_queued = {$updateQueued},
|
||||
updated_at = NOW()"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the result of an update run for a home.
|
||||
*/
|
||||
public function recordUpdateResult(int $homeId, string $status, string $error = ''): void
|
||||
{
|
||||
$status = $this->esc($status);
|
||||
$errorSql = $error !== '' ? "'" . $this->esc($error) . "'" : 'NULL';
|
||||
$successSql = $status === 'success' ? 'NOW()' : 'last_success_time';
|
||||
|
||||
$this->exec(
|
||||
"INSERT INTO `{$this->prefix}server_workshop_settings`
|
||||
(home_id, last_update_status, last_update_error, last_update_time, last_success_time)
|
||||
VALUES
|
||||
({$homeId}, '{$status}', {$errorSql}, NOW(), " . ($status === 'success' ? 'NOW()' : 'NULL') . ")
|
||||
ON DUPLICATE KEY UPDATE
|
||||
last_update_status = '{$status}',
|
||||
last_update_error = {$errorSql},
|
||||
last_update_time = NOW(),
|
||||
last_success_time = {$successSql}"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a manual update as queued (or clear the queue flag).
|
||||
*/
|
||||
public function setUpdateQueued(int $homeId, bool $queued): void
|
||||
{
|
||||
$val = $queued ? 1 : 0;
|
||||
$this->exec(
|
||||
"INSERT INTO `{$this->prefix}server_workshop_settings` (home_id, update_queued)
|
||||
VALUES ({$homeId}, {$val})
|
||||
ON DUPLICATE KEY UPDATE update_queued = {$val}"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all home IDs that have a queued manual update.
|
||||
*
|
||||
* @return array<int,int>
|
||||
*/
|
||||
public function listQueuedUpdateHomes(): array
|
||||
{
|
||||
$rows = $this->select(
|
||||
"SELECT home_id FROM `{$this->prefix}server_workshop_settings`
|
||||
WHERE update_queued = 1 AND workshop_enabled = 1"
|
||||
);
|
||||
return array_column($rows, 'home_id');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue