Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ env:
# from frontend/package-lock.json
GAMEAP_REF: main
# Ref of gameap/gameap-proto providing rust/gameap-plugin-sdk; pin to a SHA if
# the SDK drifts from Cargo.lock
GAMEAP_PROTO_REF: main
# the SDK drifts from Cargo.lock. Pinned to the head of the proto-updates
# branch: HTTPResponse.file / FileRef, which src/http.rs sets, is not on main
# yet. Restore `main` once proto-updates is merged.
GAMEAP_PROTO_REF: aba9a6c002358ed56691ff1f5c28d448e5f2cdee

jobs:
build:
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ env:
# from frontend/package-lock.json
GAMEAP_REF: main
# Ref of gameap/gameap-proto providing rust/gameap-plugin-sdk; pin to a SHA if
# the SDK drifts from Cargo.lock
GAMEAP_PROTO_REF: main
# the SDK drifts from Cargo.lock. Pinned to the head of the proto-updates
# branch: HTTPResponse.file / FileRef, which src/http.rs sets, is not on main
# yet. Restore `main` once proto-updates is merged.
GAMEAP_PROTO_REF: aba9a6c002358ed56691ff1f5c28d448e5f2cdee

jobs:
publish:
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "files"
version = "0.7.0"
version = "0.8.0"
edition = "2024"
license = "MIT"
publish = false
Expand Down
50 changes: 43 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,31 @@ FTP/SFTP daemon on nodes and manages per-server FTP users, access rules,
virtual path mounts and SSH keys.

Rust rewrite of the original Go plugin (`plugin-gameap-files`). Storage data,
node-side YAML files and the HTTP API are fully compatible — existing installs
keep working after the swap.
node-side YAML files and the HTTP API are fully compatible, so the panel side
keeps working after the swap. That compatibility does not replace the one-time
node migration every 0.7.x install still needs — see
[Upgrading from 0.7.x](#upgrading-from-07x).

*Читайте на других языках: [Русский](README_RU.md)*

## Features

- One-click gameap-files installation on nodes (chained daemon tasks, live
status tracking through daemon-task events with a poll/timeout fallback)
- One-click gameap-files installation on Linux and Windows nodes (chained
daemon tasks, live status tracking through daemon-task events with a
poll/timeout fallback) and an **Update** button that re-runs the installer
with the stored settings to upgrade an installed node
- Per-node FTP/SFTP configuration (`config.yaml` is patched in place — keys
the plugin does not own are preserved)
the plugin does not own are preserved); the service is restarted through
the system unit, the user unit of a rootless daemon, or the Windows service
- FTP/SFTP users per game server: create/update/delete, Argon2id password
hashing via the panel's crypto host service, one-time generated passwords
- Path access rules (`read` / `write` / `delete` / `list`), virtual path
mounts, SSH public keys
- Users are mirrored to nodes as hot-reloaded YAML drop-ins under
`/etc/gameap-files/users.d/`
`<work_path>/.plugins/files/users.d/` — the plugin's service directory
inside the daemon work path, the one place the daemon lets a panel plugin
write to; a relative `home_dir` is resolved against the node work path
itself, not against that directory
- Admin pages: all nodes with install status, all users grouped by
node → server with filters
- Server abilities `ftp-users-view` / `ftp-users-manage` for non-admin access
Expand Down Expand Up @@ -59,7 +67,35 @@ Panel KV storage (compatible with the Go plugin):
- `SERVER_DELETED` — removes the server's users from storage and their YAML
files from the node (node id taken from the event payload)
- `DAEMON_TASK_COMPLETED` / `DAEMON_TASK_FAILED` — matched against the
install/download task ids stored in the node's setup status
install/download task ids stored in the node's setup status; a completed
installation re-syncs every user of the node once (`synced_after_install`
in the stored status) and removes the misplaced
`<work_path>/etc/gameap-files/users.d/*.yaml` files older releases wrote

### Node layout

gameap-daemon confines a panel plugin's file operations to the node work
path, so everything this plugin writes is addressed relative to it:

| Node path (relative to `work_path`) | Contents |
|---|---|
| `.plugins/files/config.yaml` | gameap-files configuration, patched by the settings dialog |
| `.plugins/files/users.d/<user>.yaml` | one drop-in per FTP user |
| `tools/install-files-linux.sh`, `tools/install-files-windows.ps1` | installers fetched with `get-tool` |
| `tools/gameap-files/` (Windows) | binary and service of the Windows install |

### Upgrading from 0.7.x

After replacing the plugin, click **Update** once on every node installed by
an earlier release: storage and API compatibility covers the panel side only,
the node side still has to be migrated. The installer moves
`/etc/gameap-files` into `<work_path>/.plugins/files`, the plugin re-syncs the
node's users from the legacy location and sweeps the files the old release had
left under `<work_path>/etc/gameap-files`.

Until **Update** is clicked, such a node keeps reading `/etc/gameap-files`,
**Settings** on it fails with "failed to download config", and users created
in the panel are not seen by gameap-files.

## Building

Expand Down
51 changes: 44 additions & 7 deletions README_RU.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,32 @@ FTP-пользователями игровых серверов, правила
SSH-ключами.

Переписан на Rust с исходного Go-плагина (`plugin-gameap-files`). Данные в
storage, YAML-файлы на нодах и HTTP API полностью совместимы — существующие
установки продолжают работать после замены.
storage, YAML-файлы на нодах и HTTP API полностью совместимы, поэтому сторона
панели продолжает работать после замены. Эта совместимость не отменяет
разовую миграцию, которая нужна каждой ноде с 0.7.x — см.
[Обновление с 0.7.x](#обновление-с-07x).

*Read this in other languages: [English](README.md)*

## Возможности

- Установка gameap-files на ноду в один клик (цепочка daemon-тасков,
отслеживание статуса через события с fallback-поллингом и таймаутом)
- Установка gameap-files на Linux- и Windows-ноды в один клик (цепочка
daemon-тасков, отслеживание статуса через события с fallback-поллингом и
таймаутом) и кнопка **Обновить**, которая повторно запускает установщик с
сохранёнными настройками для обновления установленной ноды
- Настройка FTP/SFTP per-node (`config.yaml` патчится на месте — чужие ключи
сохраняются)
сохраняются); сервис перезапускается через системный юнит, user-юнит
rootless-демона или службу Windows
- FTP/SFTP-пользователи по серверам: создание/изменение/удаление, Argon2id-хэши
через crypto-сервис панели, одноразовый показ сгенерированного пароля
- Правила доступа по путям (`read` / `write` / `delete` / `list`),
виртуальные пути, SSH-ключи
- Синхронизация пользователей на ноды YAML-файлами в
`/etc/gameap-files/users.d/` (демон перечитывает их на лету)
`<work_path>/.plugins/files/users.d/` — сервисный каталог плагина внутри
рабочего каталога демона, единственное место, куда gameap-daemon разрешает
писать плагину панели; относительный `home_dir` раскрывается относительно
рабочего каталога ноды (`work_path`), а не этого каталога; демон
перечитывает файлы на лету
- Админ-страницы: список нод со статусами установки, все пользователи с
группировкой нода → сервер и фильтрами
- Права сервера `ftp-users-view` / `ftp-users-manage` для не-админов
Expand Down Expand Up @@ -59,7 +68,35 @@ KV-хранилище панели (совместимо с Go-версией):
- `SERVER_DELETED` — удаляет пользователей сервера из storage и их YAML с ноды
(id ноды берётся из payload события)
- `DAEMON_TASK_COMPLETED` / `DAEMON_TASK_FAILED` — сопоставляются с id
install/download-тасков из сохранённого статуса установки
install/download-тасков из сохранённого статуса установки; завершённая
установка один раз пересинхронизирует всех пользователей ноды
(`synced_after_install` в сохранённом статусе) и удаляет ошибочные файлы
`<work_path>/etc/gameap-files/users.d/*.yaml`, которые писали старые релизы

### Раскладка на ноде

gameap-daemon ограничивает файловые операции плагина панели рабочим
каталогом ноды, поэтому все пути плагина относительны к нему:

| Путь на ноде (относительно `work_path`) | Содержимое |
|---|---|
| `.plugins/files/config.yaml` | конфигурация gameap-files, патчится из диалога настроек |
| `.plugins/files/users.d/<user>.yaml` | по одному файлу на FTP-пользователя |
| `tools/install-files-linux.sh`, `tools/install-files-windows.ps1` | установщики, скачанные через `get-tool` |
| `tools/gameap-files/` (Windows) | бинарник и служба Windows-установки |

### Обновление с 0.7.x

После замены плагина нажмите **Обновить** один раз на каждой ноде,
установленной прежним релизом: совместимость storage и API покрывает только
сторону панели, ноду всё равно нужно мигрировать. Установщик переносит
`/etc/gameap-files` в `<work_path>/.plugins/files`, плагин пересинхронизирует
пользователей ноды из старого расположения и удаляет файлы, которые старый
релиз оставил в `<work_path>/etc/gameap-files`.

Пока **Обновить** не нажато, такая нода продолжает читать `/etc/gameap-files`,
**Настройки** на ней падают с ошибкой "failed to download config", а
созданные в панели пользователи не видны gameap-files.

## Сборка

Expand Down
4 changes: 2 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "plugin-files-frontend",
"version": "0.7.0",
"version": "0.8.0",
"description": "GameAP Files Plugin Frontend - FTP/SFTP Users Management",
"type": "module",
"main": "./dist/plugin.js",
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/components/admin/FtpNodeCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@
<GIcon name="users" />
<span class="ml-1">{{ trans('view_users') }}</span>
</GButton>
<GButton
color="white"
size="small"
:loading="isOperating"
:title="trans('update_installation_hint')"
@click="emit('update', node.id)"
>
<GIcon name="sync" />
<span class="ml-1">{{ trans('update_installation') }}</span>
</GButton>
<GButton color="white" size="small" :loading="isOperating" @click="emit('configure', node.id)">
<GIcon name="settings" />
<span class="ml-1">{{ trans('settings') }}</span>
Expand Down Expand Up @@ -91,6 +101,8 @@ const props = defineProps<{

const emit = defineEmits<{
setup: [nodeId: number];
/** Re-run the installer with the stored configuration (upgrade). */
update: [nodeId: number];
configure: [nodeId: number];
}>();

Expand Down
14 changes: 13 additions & 1 deletion frontend/src/components/node/NodeStatusCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,19 @@
</span>
</GButton>

<!-- Version badge and settings button -->
<!-- Version badge, update and settings buttons -->
<template v-else-if="status?.status === 'installed'">
<GStatusBadge status="success" :text="status.version" />
<GButton
color="white"
size="small"
:loading="loading"
:title="trans('update_installation_hint')"
@click="emit('update')"
>
<GIcon name="sync" />
<span class="hidden lg:inline ml-1">{{ trans('update_installation') }}</span>
</GButton>
<GButton
color="white"
size="small"
Expand Down Expand Up @@ -89,6 +99,8 @@ const props = defineProps<{

const emit = defineEmits<{
setup: [config?: NodeSetupConfig];
/** Re-run the installer with the stored configuration (upgrade). */
update: [];
loadConfig: [];
updateConfig: [config: NodeSetupConfig];
}>();
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/composables/useAdminNodes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ref } from 'vue';
import { adminApi, nodesApi } from '@/api';
import { adminApi, nodesApi, showApiError } from '@/api';
import type { AdminNode, NodeSetupConfig, NodeConfigResponse } from '@/types';

export function useAdminNodes() {
Expand Down Expand Up @@ -36,6 +36,7 @@ export function useAdminNodes() {
await fetchNodes();
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to setup node';
showApiError(e, 'Failed to setup node');
throw e;
} finally {
operatingNodeId.value = null;
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/composables/useNodeStatus.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ref, watch, onUnmounted, type Ref } from 'vue';
import { nodesApi } from '@/api';
import { nodesApi, showApiError } from '@/api';
import type { NodeSetupStatus, NodeSetupConfig, NodeConfigResponse } from '@/types';

export function useNodeStatus(nodeId: Ref<number | null>) {
Expand Down Expand Up @@ -58,6 +58,9 @@ export function useNodeStatus(nodeId: Ref<number | null>) {
}
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to start setup';
// The plugin refuses unsupported node OSes with a 400 whose message is
// the only explanation the operator gets.
showApiError(e, 'Failed to start setup');
} finally {
loading.value = false;
}
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/pages/AdminPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
:node="node"
:operating-node-id="operatingNodeId"
@setup="openSetupModal"
@update="onUpdateNode"
@configure="openConfigModal"
/>
</div>
Expand Down Expand Up @@ -128,6 +129,16 @@ async function onSetupConfirm(config: NodeSetupConfig | undefined) {
}
}

// An empty setup body re-runs the installer with the node's stored
// configuration, which is how an installed node gets upgraded.
async function onUpdateNode(nodeId: number) {
try {
await setupNode(nodeId);
} catch (e) {
console.error('Failed to update node:', e);
}
}

async function openConfigModal(nodeId: number) {
configNodeId.value = nodeId;
await fetchNodeConfig(nodeId);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/tabs/FtpUsersTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
:config="nodeConfig"
:config-loading="configLoading"
@setup="startNodeSetup"
@update="startNodeSetup()"
@load-config="loadNodeConfig"
@update-config="updateNodeConfig"
/>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const en = {
status_not_installed: 'gameap-files is not installed',
install: 'Install',
retry_setup: 'Retry Installation',
update_installation: 'Update',
update_installation_hint: 'Reinstall the latest gameap-files release; settings and users are kept',
installing: 'Installing',
node_not_ready: 'gameap-files is not installed on this node. Please contact administrator.',

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/translations/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const ru = {
status_not_installed: 'gameap-files не установлен',
install: 'Установить',
retry_setup: 'Повторить установку',
update_installation: 'Обновить',
update_installation_hint: 'Переустановить последнюю версию gameap-files; настройки и пользователи сохраняются',
installing: 'Установка',
node_not_ready: 'gameap-files не установлен на этой ноде. Обратитесь к администратору.',

Expand Down
Loading
Loading