feat: Server Content Phase 2 – schema, cache mode setting, install history, manifest, requires_stop"
Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/117ba84d-d347-410b-a634-69df74f31061 Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
parent
211875aaf3
commit
6d9cd28a0f
6 changed files with 715 additions and 49 deletions
|
|
@ -34,21 +34,68 @@ User clicks button
|
|||
|
||||
---
|
||||
|
||||
## 2. Existing Database Fields (`OGP_DB_PREFIXaddons`)
|
||||
## 2. Database Schema (db_version 4 — Phase 2)
|
||||
|
||||
### `OGP_DB_PREFIXaddons` (content item catalogue)
|
||||
|
||||
| Column | Type | Default | Description |
|
||||
|-----------------------|-----------------|---------------|-------------|
|
||||
| addon_id | INT UNSIGNED PK | auto | Primary key |
|
||||
| name | VARCHAR(80) | | Display name |
|
||||
| url | VARCHAR(200) | | Download URL (zip / tar.gz / API endpoint) |
|
||||
| path | VARCHAR(80) | | Relative target path inside server home |
|
||||
| addon_type | VARCHAR(32) | | Category key (plugin / mappack / config / …) |
|
||||
| home_cfg_id | VARCHAR(7) | | Linked game config ID |
|
||||
| post_script | LONGTEXT | | Bash script run by agent after install |
|
||||
| group_id | INT(11) NULL | | Restrict to a specific user group |
|
||||
| install_method ¹ | VARCHAR(32) | download_zip | How the content is delivered |
|
||||
| content_version ¹ | VARCHAR(64) NULL| | Version tag shown in manifest |
|
||||
| requires_stop ¹ | TINYINT(1) | 1 | Must server be stopped before install? |
|
||||
| backup_before_install¹| TINYINT(1) | 1 | Back up target path before installing? |
|
||||
| restart_after_install¹| TINYINT(1) | 0 | Restart server after successful install? |
|
||||
| is_cacheable ¹ | TINYINT(1) | 0 | Safe for cross-server copy / shared cache? |
|
||||
| description ¹ | TEXT NULL | | Short description shown to users |
|
||||
|
||||
¹ Added in db_version 4 (Phase 2).
|
||||
|
||||
### `OGP_DB_PREFIXserver_content_manifest` (Phase 2 — per-server installed state)
|
||||
|
||||
One row per (home_id, addon_id) pair. Updated on every successful install.
|
||||
|
||||
| Column | Type | Description |
|
||||
|---------------|-----------------|--------------------------------------------------|
|
||||
| addon_id | INT UNSIGNED PK | Auto-increment primary key |
|
||||
| name | VARCHAR(80) | Display name shown to users |
|
||||
| url | VARCHAR(200) | Download URL (zip / tar.gz) |
|
||||
| path | VARCHAR(80) | Relative target path inside server home |
|
||||
| addon_type | VARCHAR(32)* | Content category key (plugin / mappack / config / …) |
|
||||
| home_cfg_id | VARCHAR(7) | Linked game configuration ID |
|
||||
| post_script | LONGTEXT | Bash script run by agent after install |
|
||||
| group_id | INT(11) NULL | Restrict visibility to a specific user group |
|
||||
|-----------------|------------------|-------------|
|
||||
| id | INT UNSIGNED PK | Auto primary key |
|
||||
| home_id | INT | Server home |
|
||||
| addon_id | INT | Installed content item |
|
||||
| install_method | VARCHAR(32) | Method used for the install |
|
||||
| content_version | VARCHAR(64) NULL | Version tag at time of install |
|
||||
| install_state | VARCHAR(32) | installed / failed / removed |
|
||||
| checksum_sha256 | VARCHAR(64) NULL | SHA-256 of the installed archive |
|
||||
| source_url | VARCHAR(255) NULL| URL used for the install |
|
||||
| installed_at | DATETIME | Timestamp of last successful install |
|
||||
| installed_by | INT NULL | User ID who performed the install |
|
||||
| updated_at | DATETIME NULL | Last update time |
|
||||
| notes | TEXT NULL | Free-text admin notes |
|
||||
|
||||
\* Expanded from VARCHAR(7) to VARCHAR(32) in db_version 2
|
||||
(migration runs automatically via the module update system).
|
||||
### `OGP_DB_PREFIXserver_content_install_history` (Phase 2 — install log)
|
||||
|
||||
One row per install attempt. Never deleted (audit trail).
|
||||
|
||||
| Column | Type | Description |
|
||||
|-----------------|-------------------|-------------|
|
||||
| id | INT UNSIGNED PK | Auto primary key |
|
||||
| home_id | INT | Server home |
|
||||
| addon_id | INT | Content item installed |
|
||||
| installed_by | INT NULL | User who triggered the install |
|
||||
| started_at | DATETIME | When the install was initiated |
|
||||
| completed_at | DATETIME NULL | When the install completed |
|
||||
| install_state | VARCHAR(32) | started / installed / failed / cancelled |
|
||||
| install_method | VARCHAR(32) NULL | Method used |
|
||||
| content_version | VARCHAR(64) NULL | Version tag |
|
||||
| source_url | VARCHAR(255) NULL | Source URL used |
|
||||
| cache_mode_used | VARCHAR(32) NULL | Value of server_content_cache_mode at install time |
|
||||
| result_code | INT NULL | Script exit code (0 = success) |
|
||||
| log_output | MEDIUMTEXT NULL | Script / download log excerpt |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -161,21 +208,38 @@ Apply this as `$install_queries[2]` (db_version 3) in `module.php` when ready.
|
|||
|
||||
## 9. Recommended Phased Migration Plan
|
||||
|
||||
### Phase 1 (complete — this PR)
|
||||
### Phase 1 (complete)
|
||||
- [x] UI labels renamed to "Server Content Manager / Server Content".
|
||||
- [x] Central category map created (`server_content_categories.php`).
|
||||
- [x] `addon_type` column expanded to VARCHAR(32) via db_version 2 migration.
|
||||
- [x] `addons_installer.php` and `user_addons.php` use category map for validation.
|
||||
- [x] Full TODO/comment blocks added to installer for next phase work.
|
||||
- [x] Module folder, table names, URL routes, function names unchanged.
|
||||
- [x] Workshop Content Phase 1: manual Workshop ID entry, per-server manifest,
|
||||
agent script runner, install/update/remove actions (`db_version 3`).
|
||||
|
||||
### Phase 2 — Schema & install_method support
|
||||
- [ ] Apply the `$install_queries[2]` schema above (db_version 3).
|
||||
- [ ] Add `install_method` dropdown to admin create/edit form.
|
||||
- [ ] Implement `requires_stop` check in installer before download.
|
||||
- [ ] Implement `backup_before_install` using agent tar/zip helper.
|
||||
- [ ] Implement `restart_after_install` using existing server start logic.
|
||||
- [ ] Add install history table and log writes.
|
||||
- [x] Apply Phase 2 schema: `install_method`, `content_version`, `requires_stop`,
|
||||
`backup_before_install`, `restart_after_install`, `is_cacheable`, `description`
|
||||
columns added to `addons` table via `$install_queries[3]` (db_version 4).
|
||||
- [x] `server_content_manifest` table created – tracks one row per installed
|
||||
content item per server home (version, checksum, state, installed_by).
|
||||
- [x] `server_content_install_history` table created – one row per install
|
||||
attempt with result code, log snippet, and cache_mode_used.
|
||||
- [x] `install_method` dropdown added to admin create/edit form.
|
||||
- [x] `content_version`, `description`, `is_cacheable` fields added to form.
|
||||
- [x] `requires_stop` / `backup_before_install` / `restart_after_install`
|
||||
checkboxes added to admin form.
|
||||
- [x] `requires_stop` check implemented in installer: blocks install if server
|
||||
is running (Phase 3 will add automatic stop/start).
|
||||
- [x] Install history row written on `state=start`; completed on success.
|
||||
- [x] Manifest row upserted on successful install.
|
||||
- [x] `server_content_cache_mode` panel setting added (disabled / search_existing_servers /
|
||||
shared_cache / shared_cache_and_search; default: **disabled**).
|
||||
- [x] `is_cacheable` flag stored per content item; cache mode and is_cacheable
|
||||
guard are in place — actual cross-server/cache copy logic is Phase 3.
|
||||
- [ ] `backup_before_install` agent call (Phase 3 — needs agent tar/zip helper).
|
||||
- [ ] `restart_after_install` agent call (Phase 3 — needs server start after install).
|
||||
|
||||
### Phase 3 — Steam Workshop integration
|
||||
See Part 6 below.
|
||||
|
|
@ -295,3 +359,59 @@ Steam Workshop content is treated as a Server Content type (`addon_type=workshop
|
|||
6. **Install history must be logged.**
|
||||
- Record who installed what, when, and the script exit code.
|
||||
- This log must be readable by admins but not modifiable by users.
|
||||
|
||||
---
|
||||
|
||||
## 14. Part 10: Content Reuse / Cache Mode Security Rules
|
||||
|
||||
### Governing setting: `server_content_cache_mode`
|
||||
|
||||
| Value | Agent behaviour |
|
||||
|--------------------------|-----------------|
|
||||
| `disabled` (**default**) | Always install from the configured source/script. No cross-server copy, no shared cache. |
|
||||
| `search_existing_servers` | Agent may scan other local game-server folders for matching **cacheable** content and copy directly if the checksum matches. |
|
||||
| `shared_cache` | Agent may store **cacheable** content in a shared cache folder and use it for future installs. |
|
||||
| `shared_cache_and_search` | Both `shared_cache` and `search_existing_servers` are active. |
|
||||
|
||||
### Content eligibility for sharing / caching
|
||||
|
||||
Only content where **`is_cacheable = 1`** on the addon record may ever be
|
||||
shared or placed in the shared cache. Admins must explicitly opt in per item.
|
||||
|
||||
**Never mark the following as cacheable:**
|
||||
- Config files, server.cfg, whitelist / banlist, RCON passwords
|
||||
- User-edited files, saves, player databases
|
||||
- Log files, crash dumps
|
||||
- Files containing credentials, tokens, or keys
|
||||
- Entire server home directories
|
||||
|
||||
**Safe to mark cacheable (public, non-sensitive, reproducible):**
|
||||
- Publicly distributed map packs (.bsp, .nav, .vmf bundles)
|
||||
- Workshop content / mod archives sourced from public URLs
|
||||
- Vanilla server jars (Minecraft, etc.) from official APIs
|
||||
- Stock config templates (not user-edited copies)
|
||||
|
||||
### Checksum enforcement
|
||||
|
||||
When a cached or copied file is used, the agent **must** verify the SHA-256
|
||||
checksum against the value stored in `server_content_manifest.checksum_sha256`
|
||||
(if present). A checksum mismatch must abort the install and fall back to the
|
||||
original source download.
|
||||
|
||||
### Path restrictions
|
||||
|
||||
- The agent must never copy files **outside** approved content paths.
|
||||
- Users **cannot** specify source paths. Only admin-defined content items
|
||||
define where content comes from and where it is placed.
|
||||
- Path traversal (`../`) is forbidden in all `path` fields. The panel
|
||||
validates this; the agent provides a second layer of defence.
|
||||
- The shared cache folder must be separate from any game server home and not
|
||||
accessible directly by game server processes.
|
||||
|
||||
### Phase 3 implementation checklist (not yet done)
|
||||
|
||||
- [ ] Agent-side: implement copy-from-existing-server with checksum verification.
|
||||
- [ ] Agent-side: implement shared cache store and restore.
|
||||
- [ ] Panel-side: validate `is_cacheable` before passing cache-mode hint to agent.
|
||||
- [ ] Panel-side: expose cache hit/miss in install history log.
|
||||
- [ ] Shared cache path configurable in panel settings (`server_content_cache_path`).
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ function exec_ogp_module() {
|
|||
{
|
||||
$addon_id = (int)$_REQUEST['addon_id'];
|
||||
|
||||
$addons_rows = $db->resultQuery("SELECT url, path, post_script FROM OGP_DB_PREFIXaddons WHERE addon_id=".$addon_id.$query_groups);
|
||||
$addons_rows = $db->resultQuery("SELECT url, path, post_script, addon_type, install_method, content_version, requires_stop, restart_after_install FROM OGP_DB_PREFIXaddons WHERE addon_id=".$addon_id.$query_groups);
|
||||
if (!is_array($addons_rows)) {
|
||||
$addons_rows = [];
|
||||
}
|
||||
|
|
@ -175,6 +175,22 @@ function exec_ogp_module() {
|
|||
$remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']);
|
||||
|
||||
$addon_info = $addons_rows[0];
|
||||
$install_method = isset($addon_info['install_method']) ? $addon_info['install_method'] : 'download_zip';
|
||||
$content_version = isset($addon_info['content_version']) ? $addon_info['content_version'] : '';
|
||||
$requires_stop = !empty($addon_info['requires_stop']) ? 1 : 0;
|
||||
|
||||
// ── requires_stop guard ───────────────────────────────────────────────
|
||||
// If the content item requires the server to be stopped first, check
|
||||
// whether the server is currently running and block the install if so.
|
||||
// (Phase 2 blocks install; automatic stop/start is Phase 3.)
|
||||
if ( $state == "start" && $requires_stop ) {
|
||||
$is_running = $remote->is_screen_running( $home_info['home_name'], $home_info['home_id'] );
|
||||
if ( $is_running === 1 ) {
|
||||
print_failure('This content item requires the server to be stopped before installing. Please stop the server and try again.');
|
||||
echo "<p><a href=\"?m=addonsmanager&p=addons&addon_type=".urlencode($addon_info['addon_type'] ?? '')."&home_id=$home_id&mod_id=$mod_id&ip=$ip&port=$port\">".get_lang('back')."</a></p>";
|
||||
return;
|
||||
}
|
||||
}
|
||||
$url = $addon_info['url'];
|
||||
$filename = basename($url);
|
||||
#### Replace template variables in the post-install script with
|
||||
|
|
@ -231,8 +247,22 @@ function exec_ogp_module() {
|
|||
}
|
||||
|
||||
#### end of replacements
|
||||
if ( $state == "start" AND $addon_id != "" )
|
||||
if ( $state == "start" AND $addon_id != "" ) {
|
||||
// Record install attempt in history before triggering download.
|
||||
$cache_mode = scm_get_cache_mode($db);
|
||||
$history_id = scm_record_install_start(
|
||||
$db,
|
||||
$home_id,
|
||||
$addon_id,
|
||||
$user_id,
|
||||
$addon_info['url'],
|
||||
$content_version,
|
||||
$install_method,
|
||||
$cache_mode
|
||||
);
|
||||
$_SESSION['scm_history_id_' . $home_id . '_' . $addon_id] = $history_id;
|
||||
$pid = $remote->start_file_download( $addon_info['url'], $home_info['home_path']."/".$addon_info['path'], $filename, "uncompress", $post_script);
|
||||
}
|
||||
|
||||
$headers = get_headers($url, 1);
|
||||
|
||||
|
|
@ -301,6 +331,19 @@ function exec_ogp_module() {
|
|||
elseif( $remote->is_file_download_in_progress($pid) === 0 AND $remote->is_screen_running("post_script",$pid) === 0 )
|
||||
{
|
||||
print_success(get_lang('addon_installed_successfully'));
|
||||
// Update install history and manifest on successful completion.
|
||||
$history_key = 'scm_history_id_' . $home_id . '_' . $addon_id;
|
||||
if (!empty($_SESSION[$history_key])) {
|
||||
scm_record_install_done($db, (int)$_SESSION[$history_key], 'installed', 0);
|
||||
unset($_SESSION[$history_key]);
|
||||
}
|
||||
scm_upsert_manifest($db, $home_id, $addon_id, array(
|
||||
'install_method' => $install_method,
|
||||
'content_version' => $content_version,
|
||||
'install_state' => 'installed',
|
||||
'source_url' => $addon_info['url'],
|
||||
'installed_by' => $user_id,
|
||||
));
|
||||
echo "<p><a href=\"?m=addonsmanager&p=user_addons&home_id=$home_id".
|
||||
"&mod_id=$mod_id&ip=$ip&port=$port\">".get_lang('back')."</a></p>";
|
||||
$view->refresh("?m=addonsmanager&p=user_addons&home_id=$home_id".
|
||||
|
|
|
|||
|
|
@ -25,16 +25,21 @@
|
|||
|
||||
// Central category map — defines all valid addon_type values and their labels.
|
||||
require_once(dirname(__FILE__) . '/server_content_categories.php');
|
||||
require_once(dirname(__FILE__) . '/server_content_helpers.php');
|
||||
|
||||
function exec_ogp_module() {
|
||||
|
||||
global $db;
|
||||
|
||||
// Ensure Phase 2 schema is present (idempotent).
|
||||
scm_ensure_phase2_schema($db);
|
||||
|
||||
// Build the complete list of allowed content types from the category map.
|
||||
// Admins can create items of any registered type; the original three types
|
||||
// (plugin, mappack, config) are always included.
|
||||
$addon_types = get_server_content_type_keys(); // all keys
|
||||
$addon_type_labels = get_server_content_categories(); // key => label
|
||||
$install_methods = scm_get_install_methods(); // install_method keys => labels
|
||||
|
||||
if (isset($_POST['create_addon']) AND isset($_POST['name']) AND $_POST['url']=="")
|
||||
{
|
||||
|
|
@ -54,6 +59,7 @@ function exec_ogp_module() {
|
|||
}
|
||||
elseif (isset($_POST['create_addon']) AND isset($_POST['name']) AND isset($_POST['url']) AND isset($_POST['addon_type']) and isset($_POST['home_cfg_id']) )
|
||||
{
|
||||
$valid_install_methods = array_keys($install_methods);
|
||||
$fields['name'] = $_POST['name'];
|
||||
$fields['url'] = $_POST['url'];
|
||||
$fields['path'] = $_POST['path'];
|
||||
|
|
@ -61,6 +67,13 @@ function exec_ogp_module() {
|
|||
$fields['home_cfg_id'] = $_POST['home_cfg_id'];
|
||||
$fields['post_script'] = $_POST['post_script'];
|
||||
$fields['group_id'] = $_POST['group_id'];
|
||||
$fields['install_method'] = in_array($_POST['install_method'], $valid_install_methods) ? $_POST['install_method'] : 'download_zip';
|
||||
$fields['content_version'] = isset($_POST['content_version']) ? $_POST['content_version'] : '';
|
||||
$fields['requires_stop'] = !empty($_POST['requires_stop']) ? 1 : 0;
|
||||
$fields['backup_before_install'] = !empty($_POST['backup_before_install']) ? 1 : 0;
|
||||
$fields['restart_after_install'] = !empty($_POST['restart_after_install']) ? 1 : 0;
|
||||
$fields['is_cacheable'] = !empty($_POST['is_cacheable']) ? 1 : 0;
|
||||
$fields['description'] = isset($_POST['description']) ? $_POST['description'] : '';
|
||||
if( is_numeric($db->resultInsertId( 'addons', $fields )) )
|
||||
{
|
||||
print_success(get_lang_f("addon_has_been_created",$_POST['name']));
|
||||
|
|
@ -77,6 +90,13 @@ function exec_ogp_module() {
|
|||
$home_cfg_id = isset($_POST['home_cfg_id']) ? $_POST['home_cfg_id'] : "";
|
||||
$addon_type = isset($_POST['addon_type']) ? $_POST['addon_type'] : "";
|
||||
$group_id = isset($_POST['group_id']) ? $_POST['group_id'] : "";
|
||||
$install_method = isset($_POST['install_method']) ? $_POST['install_method'] : "download_zip";
|
||||
$content_version = isset($_POST['content_version']) ? $_POST['content_version'] : "";
|
||||
$requires_stop = isset($_POST['requires_stop']) ? (int)$_POST['requires_stop'] : 1;
|
||||
$backup_before_install = isset($_POST['backup_before_install']) ? (int)$_POST['backup_before_install'] : 1;
|
||||
$restart_after_install = isset($_POST['restart_after_install']) ? (int)$_POST['restart_after_install'] : 0;
|
||||
$is_cacheable = isset($_POST['is_cacheable']) ? (int)$_POST['is_cacheable'] : 0;
|
||||
$description = isset($_POST['description']) ? $_POST['description'] : "";
|
||||
|
||||
if (isset($_POST['addon_id']) && (int)$_POST['addon_id'] > 0 && isset($_POST['edit']))
|
||||
{
|
||||
|
|
@ -92,6 +112,13 @@ function exec_ogp_module() {
|
|||
$home_cfg_id = isset($addon_info['home_cfg_id']) ? $addon_info['home_cfg_id'] : "";
|
||||
$addon_type = isset($addon_info['addon_type']) ? $addon_info['addon_type'] : "";
|
||||
$group_id = isset($addon_info['group_id']) ? $addon_info['group_id'] : "";
|
||||
$install_method = isset($addon_info['install_method']) ? $addon_info['install_method'] : "download_zip";
|
||||
$content_version = isset($addon_info['content_version']) ? $addon_info['content_version'] : "";
|
||||
$requires_stop = isset($addon_info['requires_stop']) ? (int)$addon_info['requires_stop'] : 1;
|
||||
$backup_before_install = isset($addon_info['backup_before_install']) ? (int)$addon_info['backup_before_install'] : 1;
|
||||
$restart_after_install = isset($addon_info['restart_after_install']) ? (int)$addon_info['restart_after_install'] : 0;
|
||||
$is_cacheable = isset($addon_info['is_cacheable']) ? (int)$addon_info['is_cacheable'] : 0;
|
||||
$description = isset($addon_info['description']) ? $addon_info['description'] : "";
|
||||
}
|
||||
?>
|
||||
<form action="" method="post">
|
||||
|
|
@ -214,6 +241,79 @@ function exec_ogp_module() {
|
|||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- ── Phase 2 fields ────────────────────────────────────────────────── -->
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b>Install Method</b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<select name="install_method">
|
||||
<?php
|
||||
foreach ((array)$install_methods as $method_key => $method_label) {
|
||||
$sel = ($method_key == $install_method) ? 'selected="selected"' : '';
|
||||
echo '<option value="'.htmlspecialchars($method_key).'" '.$sel.'>'.htmlspecialchars($method_label).'</option>'."\n";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<small style="color:#666;"> The mechanism used to deliver this content to the server.</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b>Content Version</b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<input type="text" value="<?php echo htmlspecialchars($content_version, ENT_QUOTES, 'UTF-8'); ?>" name="content_version" size="40" placeholder="e.g. 1.21.1 or 2024-05-01" />
|
||||
<small style="color:#666;"> Optional version tag shown in the installed-content list.</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b>Description</b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<textarea name="description" style="width:99%;height:60px;" placeholder="Short description shown to users."><?php echo htmlspecialchars($description, ENT_QUOTES, 'UTF-8'); ?></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b>Behaviour Options</b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<label>
|
||||
<input type="checkbox" name="requires_stop" value="1" <?php echo $requires_stop ? 'checked' : ''; ?> />
|
||||
Stop server before installing
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="backup_before_install" value="1" <?php echo $backup_before_install ? 'checked' : ''; ?> />
|
||||
Backup target path before installing
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="restart_after_install" value="1" <?php echo $restart_after_install ? 'checked' : ''; ?> />
|
||||
Restart server after successful install
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">
|
||||
<b>Content Reuse</b>
|
||||
</td>
|
||||
<td align="left">
|
||||
<label>
|
||||
<input type="checkbox" name="is_cacheable" value="1" <?php echo $is_cacheable ? 'checked' : ''; ?> />
|
||||
Mark as cacheable / reusable
|
||||
</label>
|
||||
<small style="color:#666;">
|
||||
Only check this for public, non-sensitive content (maps, mods, jars).
|
||||
<strong>Never</strong> check for configs, saves, credentials, or user-edited files.
|
||||
Caching only activates when the <em>Server Content Cache Mode</em> panel
|
||||
setting (in Panel Settings) is set to something other than <em>Disabled</em>.
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- ── end Phase 2 fields ─────────────────────────────────────────────── -->
|
||||
<tr>
|
||||
<td colspan="2" align="center">
|
||||
<?php
|
||||
|
|
|
|||
|
|
@ -13,13 +13,18 @@
|
|||
* 1 – initial schema (addons table, addon_type VARCHAR(7))
|
||||
* 2 – expand addon_type to VARCHAR(32) to support extended content types
|
||||
* (workshop=8 chars, and any future type up to 32 chars)
|
||||
* 3 – add server_content_workshop table for per-server Workshop item selections
|
||||
* 4 – Phase 2: add install_method / content_version / requires_stop /
|
||||
* backup_before_install / restart_after_install / is_cacheable /
|
||||
* description columns to addons table; add server_content_manifest
|
||||
* and server_content_install_history tables
|
||||
*
|
||||
*/
|
||||
|
||||
// Module general information
|
||||
$module_title = "Server Content Manager";
|
||||
$module_version = "2.1";
|
||||
$db_version = 3;
|
||||
$module_version = "2.2";
|
||||
$db_version = 4;
|
||||
$module_required = TRUE;
|
||||
$module_menus = array(
|
||||
array( 'subpage' => 'addons_manager', 'name' => 'Server Content Manager', 'group' => 'admin' )
|
||||
|
|
@ -72,4 +77,92 @@ $install_queries[2] = array(
|
|||
KEY `idx_install_state` (`install_state`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"
|
||||
);
|
||||
|
||||
// ── db_version 4 : Phase 2 – install_method, per-server manifest, install history ──
|
||||
//
|
||||
// Uses a PHP callable so each ALTER is applied only when the column does not
|
||||
// already exist (safe for repeated runs, compatible with all MySQL versions).
|
||||
//
|
||||
$install_queries[3] = array(
|
||||
function ($db) {
|
||||
$prefix = OGP_DB_PREFIX;
|
||||
|
||||
// ── Extend the addons table with Phase 2 columns ──────────────────────
|
||||
$new_columns = array(
|
||||
'install_method' => "VARCHAR(32) NOT NULL DEFAULT 'download_zip' AFTER `group_id`",
|
||||
'content_version' => "VARCHAR(64) NULL AFTER `install_method`",
|
||||
'requires_stop' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `content_version`",
|
||||
'backup_before_install' => "TINYINT(1) NOT NULL DEFAULT 1 AFTER `requires_stop`",
|
||||
'restart_after_install' => "TINYINT(1) NOT NULL DEFAULT 0 AFTER `backup_before_install`",
|
||||
'is_cacheable' => "TINYINT(1) NOT NULL DEFAULT 0 AFTER `restart_after_install`",
|
||||
'description' => "TEXT NULL AFTER `is_cacheable`",
|
||||
);
|
||||
|
||||
foreach ($new_columns as $col => $definition) {
|
||||
$escaped_col = $db->realEscapeSingle($col);
|
||||
$escaped_table = $db->realEscapeSingle($prefix . 'addons');
|
||||
$check = $db->resultQuery(
|
||||
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = '{$escaped_table}'
|
||||
AND COLUMN_NAME = '{$escaped_col}'"
|
||||
);
|
||||
if (empty($check)) {
|
||||
if (!$db->query("ALTER TABLE `{$prefix}addons` ADD COLUMN `{$col}` {$definition}")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Per-server installed-content manifest ─────────────────────────────
|
||||
if (!$db->query(
|
||||
"CREATE TABLE IF NOT EXISTS `{$prefix}server_content_manifest` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`home_id` INT NOT NULL,
|
||||
`addon_id` INT NOT NULL,
|
||||
`install_method` VARCHAR(32) NOT NULL DEFAULT 'download_zip',
|
||||
`content_version` VARCHAR(64) NULL,
|
||||
`install_state` VARCHAR(32) NOT NULL DEFAULT 'installed',
|
||||
`checksum_sha256` VARCHAR(64) NULL,
|
||||
`source_url` VARCHAR(255) NULL,
|
||||
`installed_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`installed_by` INT NULL,
|
||||
`updated_at` DATETIME NULL,
|
||||
`notes` TEXT NULL,
|
||||
UNIQUE KEY `uniq_home_addon` (`home_id`, `addon_id`),
|
||||
KEY `idx_home_id` (`home_id`),
|
||||
KEY `idx_addon_id` (`addon_id`),
|
||||
KEY `idx_install_state` (`install_state`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Install history (one row per install attempt) ─────────────────────
|
||||
if (!$db->query(
|
||||
"CREATE TABLE IF NOT EXISTS `{$prefix}server_content_install_history` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`home_id` INT NOT NULL,
|
||||
`addon_id` INT NOT NULL,
|
||||
`installed_by` INT NULL,
|
||||
`started_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`completed_at` DATETIME NULL,
|
||||
`install_state` VARCHAR(32) NOT NULL DEFAULT 'started',
|
||||
`install_method` VARCHAR(32) NULL,
|
||||
`content_version` VARCHAR(64) NULL,
|
||||
`source_url` VARCHAR(255) NULL,
|
||||
`cache_mode_used` VARCHAR(32) NULL,
|
||||
`result_code` INT NULL,
|
||||
`log_output` MEDIUMTEXT NULL,
|
||||
KEY `idx_home_id` (`home_id`),
|
||||
KEY `idx_addon_id` (`addon_id`),
|
||||
KEY `idx_started_at` (`started_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
);
|
||||
?>
|
||||
|
|
|
|||
|
|
@ -198,3 +198,295 @@ function scm_validate_csrf_token($token)
|
|||
return hash_equals((string)$_SESSION['addonsmanager_workshop_csrf'], (string)$token);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Phase 2 helpers – schema guard, cache mode, manifest, install history
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Returns the allowed values for the server_content_cache_mode panel setting.
|
||||
*
|
||||
* disabled – Always install from the configured source. No scanning,
|
||||
* no shared cache. (DEFAULT – safest choice)
|
||||
* search_existing_servers – Agent may scan other local game-server folders for
|
||||
* matching cacheable content and copy directly if safe.
|
||||
* shared_cache – Agent may store cacheable content in a shared cache
|
||||
* folder and reuse it on future installs.
|
||||
* shared_cache_and_search – Both shared_cache and search_existing_servers are
|
||||
* active simultaneously.
|
||||
*
|
||||
* Security note: only content explicitly marked is_cacheable=1 on the addon
|
||||
* record may ever be shared or cached. Private configs, user-edited files,
|
||||
* saves, databases, logs, and credentials must never be included.
|
||||
*
|
||||
* @return array<string,string> key => human-readable label
|
||||
*/
|
||||
function scm_get_valid_cache_modes()
|
||||
{
|
||||
return array(
|
||||
'disabled' => 'Disabled (always install from source)',
|
||||
'search_existing_servers' => 'Search existing servers (copy from local installs)',
|
||||
'shared_cache' => 'Shared cache (store and reuse cached copies)',
|
||||
'shared_cache_and_search' => 'Shared cache + search existing servers',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the current server_content_cache_mode panel setting.
|
||||
* Returns 'disabled' if not set.
|
||||
*
|
||||
* @param object $db Panel DB handle
|
||||
* @return string One of the scm_get_valid_cache_modes() keys
|
||||
*/
|
||||
function scm_get_cache_mode($db)
|
||||
{
|
||||
$valid = scm_get_valid_cache_modes();
|
||||
$value = '';
|
||||
if (method_exists($db, 'getSetting')) {
|
||||
$value = (string)$db->getSetting('server_content_cache_mode');
|
||||
}
|
||||
return array_key_exists($value, $valid) ? $value : 'disabled';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns allowed install_method values and their display labels.
|
||||
*
|
||||
* @return array<string,string>
|
||||
*/
|
||||
function scm_get_install_methods()
|
||||
{
|
||||
return array(
|
||||
'download_zip' => 'Download & extract archive (.zip / .tar.gz)',
|
||||
'download_file' => 'Download single file (no extraction)',
|
||||
'post_script' => 'Run post-script only (no download)',
|
||||
'steam_workshop' => 'Steam Workshop (via agent SteamCMD)',
|
||||
'minecraft_jar' => 'Minecraft server jar / version switcher',
|
||||
'profile_copy' => 'Copy stored profile directory',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Idempotently ensures the Phase 2 schema is present.
|
||||
* Called from pages that use manifest / history data so that existing
|
||||
* installs that have not yet run the module updater are covered.
|
||||
*
|
||||
* @param object $db Panel DB handle
|
||||
* @return bool
|
||||
*/
|
||||
function scm_ensure_phase2_schema($db)
|
||||
{
|
||||
static $phase2_checked = false;
|
||||
if ($phase2_checked) {
|
||||
return true;
|
||||
}
|
||||
$phase2_checked = true;
|
||||
$prefix = OGP_DB_PREFIX;
|
||||
|
||||
// ── Extend addons table ───────────────────────────────────────────────────
|
||||
$new_columns = array(
|
||||
'install_method' => "VARCHAR(32) NOT NULL DEFAULT 'download_zip'",
|
||||
'content_version' => "VARCHAR(64) NULL",
|
||||
'requires_stop' => "TINYINT(1) NOT NULL DEFAULT 1",
|
||||
'backup_before_install' => "TINYINT(1) NOT NULL DEFAULT 1",
|
||||
'restart_after_install' => "TINYINT(1) NOT NULL DEFAULT 0",
|
||||
'is_cacheable' => "TINYINT(1) NOT NULL DEFAULT 0",
|
||||
'description' => "TEXT NULL",
|
||||
);
|
||||
foreach ($new_columns as $col => $definition) {
|
||||
$escaped_col = $db->realEscapeSingle($col);
|
||||
$escaped_table = $db->realEscapeSingle($prefix . 'addons');
|
||||
$check = $db->resultQuery(
|
||||
"SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = '{$escaped_table}'
|
||||
AND COLUMN_NAME = '{$escaped_col}'"
|
||||
);
|
||||
if (empty($check)) {
|
||||
$db->query("ALTER TABLE `{$prefix}addons` ADD COLUMN `{$col}` {$definition}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Per-server manifest ───────────────────────────────────────────────────
|
||||
$db->query(
|
||||
"CREATE TABLE IF NOT EXISTS `{$prefix}server_content_manifest` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`home_id` INT NOT NULL,
|
||||
`addon_id` INT NOT NULL,
|
||||
`install_method` VARCHAR(32) NOT NULL DEFAULT 'download_zip',
|
||||
`content_version` VARCHAR(64) NULL,
|
||||
`install_state` VARCHAR(32) NOT NULL DEFAULT 'installed',
|
||||
`checksum_sha256` VARCHAR(64) NULL,
|
||||
`source_url` VARCHAR(255) NULL,
|
||||
`installed_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`installed_by` INT NULL,
|
||||
`updated_at` DATETIME NULL,
|
||||
`notes` TEXT NULL,
|
||||
UNIQUE KEY `uniq_home_addon` (`home_id`, `addon_id`),
|
||||
KEY `idx_home_id` (`home_id`),
|
||||
KEY `idx_addon_id` (`addon_id`),
|
||||
KEY `idx_install_state` (`install_state`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||
);
|
||||
|
||||
// ── Install history ───────────────────────────────────────────────────────
|
||||
$db->query(
|
||||
"CREATE TABLE IF NOT EXISTS `{$prefix}server_content_install_history` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`home_id` INT NOT NULL,
|
||||
`addon_id` INT NOT NULL,
|
||||
`installed_by` INT NULL,
|
||||
`started_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`completed_at` DATETIME NULL,
|
||||
`install_state` VARCHAR(32) NOT NULL DEFAULT 'started',
|
||||
`install_method` VARCHAR(32) NULL,
|
||||
`content_version` VARCHAR(64) NULL,
|
||||
`source_url` VARCHAR(255) NULL,
|
||||
`cache_mode_used` VARCHAR(32) NULL,
|
||||
`result_code` INT NULL,
|
||||
`log_output` MEDIUMTEXT NULL,
|
||||
KEY `idx_home_id` (`home_id`),
|
||||
KEY `idx_addon_id` (`addon_id`),
|
||||
KEY `idx_started_at` (`started_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all manifest rows for a given server home.
|
||||
*
|
||||
* @param object $db
|
||||
* @param int $home_id
|
||||
* @return array
|
||||
*/
|
||||
function scm_get_manifest_rows($db, $home_id)
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
if ($home_id <= 0 || !scm_ensure_phase2_schema($db)) {
|
||||
return array();
|
||||
}
|
||||
$rows = $db->resultQuery(
|
||||
"SELECT m.*, a.name AS addon_name, a.addon_type, a.install_method AS addon_install_method
|
||||
FROM `".OGP_DB_PREFIX."server_content_manifest` m
|
||||
LEFT JOIN `".OGP_DB_PREFIX."addons` a ON a.addon_id = m.addon_id
|
||||
WHERE m.home_id = {$home_id}
|
||||
ORDER BY m.installed_at DESC"
|
||||
);
|
||||
return is_array($rows) ? $rows : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new install history row and returns its insert ID.
|
||||
* Returns 0 on failure.
|
||||
*
|
||||
* @param object $db
|
||||
* @param int $home_id
|
||||
* @param int $addon_id
|
||||
* @param int $user_id
|
||||
* @param string $source_url
|
||||
* @param string $content_version
|
||||
* @param string $install_method
|
||||
* @param string $cache_mode_used
|
||||
* @return int history row ID, or 0 on failure
|
||||
*/
|
||||
function scm_record_install_start($db, $home_id, $addon_id, $user_id, $source_url = '', $content_version = '', $install_method = 'download_zip', $cache_mode_used = 'disabled')
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
$addon_id = (int)$addon_id;
|
||||
$user_id = (int)$user_id;
|
||||
$source_url = $db->realEscapeSingle((string)$source_url);
|
||||
$content_version = $db->realEscapeSingle((string)$content_version);
|
||||
$install_method = $db->realEscapeSingle((string)$install_method);
|
||||
$cache_mode_used = $db->realEscapeSingle((string)$cache_mode_used);
|
||||
|
||||
if (!scm_ensure_phase2_schema($db)) {
|
||||
return 0;
|
||||
}
|
||||
$id = $db->resultInsertId(
|
||||
'server_content_install_history',
|
||||
array(
|
||||
'home_id' => $home_id,
|
||||
'addon_id' => $addon_id,
|
||||
'installed_by' => $user_id,
|
||||
'install_state' => 'started',
|
||||
'install_method' => $install_method,
|
||||
'content_version' => $content_version,
|
||||
'source_url' => $source_url,
|
||||
'cache_mode_used' => $cache_mode_used,
|
||||
)
|
||||
);
|
||||
return is_numeric($id) ? (int)$id : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing install history row with the final result.
|
||||
*
|
||||
* @param object $db
|
||||
* @param int $history_id
|
||||
* @param string $state 'installed' | 'failed' | 'cancelled'
|
||||
* @param int $result_code Exit code (0 = success)
|
||||
* @param string $log_output Script/download log snippet
|
||||
* @return bool
|
||||
*/
|
||||
function scm_record_install_done($db, $history_id, $state = 'installed', $result_code = 0, $log_output = '')
|
||||
{
|
||||
$history_id = (int)$history_id;
|
||||
$state = $db->realEscapeSingle((string)$state);
|
||||
$result_code = (int)$result_code;
|
||||
$log_output = $db->realEscapeSingle((string)$log_output);
|
||||
if ($history_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
return (bool)$db->query(
|
||||
"UPDATE `".OGP_DB_PREFIX."server_content_install_history`
|
||||
SET install_state = '{$state}',
|
||||
result_code = {$result_code},
|
||||
log_output = '{$log_output}',
|
||||
completed_at = NOW()
|
||||
WHERE id = {$history_id}"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts or updates a server_content_manifest row for a successful install.
|
||||
*
|
||||
* @param object $db
|
||||
* @param int $home_id
|
||||
* @param int $addon_id
|
||||
* @param array $fields Optional overrides: install_method, content_version,
|
||||
* install_state, source_url, checksum_sha256, installed_by
|
||||
* @return bool
|
||||
*/
|
||||
function scm_upsert_manifest($db, $home_id, $addon_id, array $fields = array())
|
||||
{
|
||||
$home_id = (int)$home_id;
|
||||
$addon_id = (int)$addon_id;
|
||||
if ($home_id <= 0 || $addon_id <= 0 || !scm_ensure_phase2_schema($db)) {
|
||||
return false;
|
||||
}
|
||||
$install_method = $db->realEscapeSingle((string)(isset($fields['install_method']) ? $fields['install_method'] : 'download_zip'));
|
||||
$content_version = $db->realEscapeSingle((string)(isset($fields['content_version']) ? $fields['content_version'] : ''));
|
||||
$install_state = $db->realEscapeSingle((string)(isset($fields['install_state']) ? $fields['install_state'] : 'installed'));
|
||||
$source_url = $db->realEscapeSingle((string)(isset($fields['source_url']) ? $fields['source_url'] : ''));
|
||||
$checksum = $db->realEscapeSingle((string)(isset($fields['checksum_sha256']) ? $fields['checksum_sha256'] : ''));
|
||||
$installed_by = isset($fields['installed_by']) ? (int)$fields['installed_by'] : 'NULL';
|
||||
if ($installed_by !== 'NULL' && $installed_by <= 0) {
|
||||
$installed_by = 'NULL';
|
||||
}
|
||||
|
||||
return (bool)$db->query(
|
||||
"INSERT INTO `".OGP_DB_PREFIX."server_content_manifest`
|
||||
(`home_id`,`addon_id`,`install_method`,`content_version`,`install_state`,`source_url`,`checksum_sha256`,`installed_by`,`installed_at`,`updated_at`)
|
||||
VALUES
|
||||
({$home_id},{$addon_id},'{$install_method}','{$content_version}','{$install_state}','{$source_url}','{$checksum}',{$installed_by},NOW(),NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
install_method = VALUES(install_method),
|
||||
content_version = VALUES(content_version),
|
||||
install_state = VALUES(install_state),
|
||||
source_url = VALUES(source_url),
|
||||
checksum_sha256 = VALUES(checksum_sha256),
|
||||
installed_at = NOW(),
|
||||
updated_at = NOW()"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,9 @@ function exec_ogp_module()
|
|||
"discord_webhook_main" => $_REQUEST['discord_webhook_main'],
|
||||
"discord_webhook_admin" => $_REQUEST['discord_webhook_admin'],
|
||||
// Debug
|
||||
"debug_level" => $_REQUEST['debug_level'] ?? '1'
|
||||
"debug_level" => $_REQUEST['debug_level'] ?? '1',
|
||||
// Server Content
|
||||
"server_content_cache_mode" => $_REQUEST['server_content_cache_mode'] ?? 'disabled',
|
||||
);
|
||||
|
||||
$db->setSettings($settings);
|
||||
|
|
@ -208,6 +210,22 @@ function exec_ogp_module()
|
|||
$ft->add_custom_field('debug_level',
|
||||
create_drop_box_from_array($debug_level_options, 'debug_level', @$row['debug_level'] ?? '1', false));
|
||||
|
||||
// Server Content cache mode
|
||||
// Controls whether the agent may copy cacheable content from other servers
|
||||
// or a shared cache instead of always re-downloading from the source.
|
||||
// Default: disabled (safest – no copying of any content between servers).
|
||||
// See addonsmanager/SERVER_CONTENT_ROADMAP.md for full description.
|
||||
$cache_mode_options = array(
|
||||
'disabled' => 'Disabled (always install from source — default)',
|
||||
'search_existing_servers' => 'Search existing servers (copy cacheable content from local game-server folders)',
|
||||
'shared_cache' => 'Shared cache (store and reuse cacheable content in a shared cache folder)',
|
||||
'shared_cache_and_search' => 'Shared cache + search existing servers',
|
||||
);
|
||||
$current_cache_mode = isset($row['server_content_cache_mode']) && array_key_exists($row['server_content_cache_mode'], $cache_mode_options)
|
||||
? $row['server_content_cache_mode'] : 'disabled';
|
||||
$ft->add_custom_field('server_content_cache_mode',
|
||||
create_drop_box_from_array($cache_mode_options, 'server_content_cache_mode', $current_cache_mode, false));
|
||||
|
||||
// Add option to reset game server order to default
|
||||
$ft->add_field('checkbox','reset_game_server_order','0');
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue