Files
simple-updater/README.md
T
2026-09-12 03:11:49 +03:00

175 lines
15 KiB
Markdown

# simple-updater
Лёгкий self-hosted обновлятор для игры/приложения: HTTP-сервер, который раздаёт файлы клиентам, и Windows-обновлятор (`updater.py` → `updater.exe`), который сравнивает локальные файлы с манифестом на сервере и докачивает то, что изменилось.
## Возможности
- **Раздача нескольких версий клиента** с одного сервера (например `Main` и `Test`) через отдельные access-ключи в URL.
- **Подпись манифестов (Ed25519)** — сервер не может незаметно подменить список файлов или их хэши; обновлятор проверяет подпись публичным ключом перед тем, как довериться манифесту.
- **Сжатие файлов** — при генерации манифеста каждый файл сжимается gzip'ом; если сжатая копия меньше оригинала, обновлятор скачивает её и разжимает локально. Повторная генерация манифеста пересжимает только реально изменившиеся файлы (см. «Сжатие» ниже).
- **Self-update** — сам `updater.exe` умеет проверять и устанавливать собственные новые версии, без отдельной ручной раздачи.
- **Единый `config.json`** — все настройки (сервер, клиенты, ключи, вид окна, лимиты) в одном файле на сервере.
- **`quickstart.py`** — генерирует ключи и манифесты одной командой на сервере.
- **`build_client.py`** — отдельно собирает `updater.exe` на Windows, забирая всё нужное с сервера по сети, без доступа к серверному `config.json` целиком.
## Архитектура: сервер и клиентская сборка — раздельно
PyInstaller не кросс-компилирует: собранный на Linux/macOS «updater.exe» — это бинарник для той ОС, на которой его собрали, просто с расширением `.exe`; на Windows он не запустится. Поэтому в проекте два независимых пайплайна:
- **Серверная часть** (`quickstart.py`, `generate_key.py`, `generate_manifest.py`, `generate_updater_manifest.py`, `server.py`) — работает на любой ОС, обычно Linux. Генерирует ключи, манифесты, раздаёт файлы. Никогда не собирает exe.
- **Клиентская сборка** (`build_client.py`) — работает **только на Windows** (реальной машине, VM или CI-раннере). Ничего не генерирует сама: забирает access-ключи, публичный ключ подписи и остальные настройки с работающего сервера по HTTP(S) и собирает `updater.exe`.
Приватный ключ подписи манифестов никогда не покидает сервер — `build_client.py` физически не может его получить (сервер его и не отдаёт, см. ниже).
## Быстрый старт
**На сервере:**
```bash
pip install -r requirements.txt
# сгенерировать ключи + положить файлы игры в game_client/ + собрать манифесты
python quickstart.py
# запустить сервер
python server.py
```
Первый запуск `quickstart.py` создаст пустые папки клиентов (`game_client`, `game_client_test` — имена берутся из `config.json`'s `clients`), сгенерирует access-ключи и пару ключей для подписи манифестов. Положите файлы игры в `game_client/`, запустите `quickstart.py` ещё раз — манифест сгенерируется и подпишется автоматически.
**На Windows-машине для сборки клиента:**
```bash
pip install -r requirements-client.txt
python build_client.py --server-url https://example.com:443/ --save-server-url
```
Скрипт скачает нужные настройки с сервера и соберёт `updater.exe` в `dist/`. При повторных запусках `--server-url` можно не указывать — адрес запоминается локально. Готовый exe нужно вручную скопировать в клиентские папки на сервере (или сразу опубликовать как self-update — см. «Ручной запуск отдельных шагов»).
## config.json (сервер)
Все настройки серверной части живут здесь, вложенными секциями:
| Секция | Что настраивает |
|---|---|
| `server` | host, port, лимит соединений на IP |
| `clients` | имя клиента → папка с файлами (`"Main": "game_client"`) |
| `access_keys` | имя клиента → access-ключ (генерируется автоматически) |
| `default_client` | какой клиент выбран в обновляторе по умолчанию |
| `signing_keys` | публичный ключ подписи + имя файла с приватным ключом |
| `require_signature` | обязательна ли подпись манифеста (по умолчанию да) |
| `connection` | адрес и порт сервера, HTTPS вкл/выкл — то, к чему подключается обновлятор |
| `updater_ui` | название игры, размеры окна и баннера |
| `paths` | путь к exe игры, баннеру, иконке, файлу настроек |
| `manifest` | имя файла манифеста, настройки сжатия, лимит размера файла |
| `ignore` | какие файлы/папки/расширения не попадают в манифест |
| `self_update` | включён ли self-update, текущая версия обновлятора |
Чтобы что-то поменять — правьте `config.json` и перезапускайте `quickstart.py` (для ключей/манифестов) или `server.py` (для настроек сервера). Ручное редактирование `updater.py`/`generate_manifest.py` не нужно.
### Приватный ключ подписи
Единственный настоящий секрет в проекте — приватный ключ подписи манифестов. Он **не хранится в `config.json`**, а лежит в отдельном файле `SIGNING_PRIVATE_KEY.txt` на сервере (имя настраивается через `signing_keys.private_key_file`). Публичный ключ — в `config.json`, его можно раскрывать сколько угодно, зашивать в exe и т.д.
Держите `SIGNING_PRIVATE_KEY.txt` только на сервере — не копируйте его вместе с остальным проектом, добавьте в `.gitignore`, если используете git. `build_client.py` его никогда не запрашивает и не получает: сервер отдаёт клиентским сборкам только `client_build_info.json` (см. ниже), где приватного ключа нет по построению.
### /client_build_info.json
`server.py` отдаёт этот путь без access-ключа — это весь `config.json`, кроме секции `server` (host/port/лимиты — внутренняя топология сервера, не нужна сборке). Всё остальное содержимое всё равно окажется зашитым в `updater.exe` открытым текстом, так что прятать его за access-ключом смысла нет. Приватного ключа подписи там нет ни при каких обстоятельствах — он физически не входит в `config.json`.
## Сжатие (.gz)
При генерации манифеста каждый файл пробуется сжать gzip'ом; если сжатая копия меньше оригинала, она попадает в манифест и раздаётся вместо исходного файла.
- **Отдельная папка для `.gz`** — по умолчанию `manifest.compressed_dir` в `config.json` равен `"_gz"`, то есть сжатые копии лежат в `game_client/_gz/...`, а не рядом с оригиналами. `updater.py` при этом не меняется: он как и раньше запрашивает `system/game.exe.gz` напрямую — `server.py` сам подставляет реальный путь из `_gz/`, если файла нет на «прямом» месте. Поставьте `compressed_dir` в `""`, чтобы вернуть старое поведение (файлы рядом с оригиналом).
- **Кэш сжатия** — повторный запуск `generate_manifest.py` на той же папке пересжимает только файлы, у которых изменился хэш содержимого с прошлого раза (сверяется со старым `manifest.json`). Не изменившиеся файлы просто переиспользуют старые `compressed_hash`/`compressed_size` без повторного gzip. Это делает частую регенерацию манифеста на большом клиенте быстрой.
- **Принудительное пересжатие** — `python generate_manifest.py game_client --force-recompress` игнорирует кэш и пересжимает всё заново (например, после смены `compression_level` в `config.json`).
- **Очистка осиротевших `.gz`** — после каждой генерации манифеста автоматически удаляются `.gz`-файлы, которые больше ни на что не ссылаются: оригинал удалили/переименовали, файл перестал быть выгодным для сжатия, или сменили `compressed_dir` (старые копии в прежнем месте подчищаются). Без этого `.gz`-файлы только копились бы на диске. Отключить можно флагом `--no-clean-orphans`, если по какой-то причине это нежелательно.
## Как всё это работает вместе
```
СЕРВЕР:
config.json ──┬──> generate_key.py ────────> access-ключи в config.json
│ приватный ключ в SIGNING_PRIVATE_KEY.txt
│ публичный ключ в config.json
│
├──> generate_manifest.py ───> game_client/manifest.json + .sig
│ game_client/_gz/... (сжатые копии)
│
├──> generate_updater_manifest.py ──> game_client/updater_manifest.json + .sig
│
└──> quickstart.py ──> запускает все шаги выше по порядку
server.py ──> раздаёт клиентские папки по HTTP(S) под access-ключом в URL
──> отдаёт /client_build_info.json (без access-ключа, без приватного ключа)
WINDOWS (отдельная машина):
build_client.py ──> тянет /client_build_info.json ──> патчит updater.py ──> updater.exe
ИГРОК:
updater.py / updater.exe ──> скачивает манифест, проверяет подпись,
докачивает изменившиеся файлы, запускает игру
```
## Ручной запуск отдельных шагов
Если `quickstart.py`/`build_client.py` не подходят, каждый скрипт можно запускать по отдельности.
На сервере:
```bash
# access-ключ для клиента, сохранить в config.json
python generate_key.py --client Main --save
# пара ключей для подписи манифестов
python generate_key.py --signing-keypair --save
# сгенерировать и подписать манифест для одной клиентской папки
python generate_manifest.py game_client
# то же самое, но пересжать вообще все файлы, игнорируя кэш
python generate_manifest.py game_client --force-recompress
# сгенерировать манифест, но не трогать осиротевшие .gz файлы на диске
python generate_manifest.py game_client --no-clean-orphans
# опубликовать exe (уже собранный на Windows) как новую self-update версию
python generate_updater_manifest.py updater.exe game_client --version 2
# запустить сервер
python server.py
```
На Windows:
```bash
# первый раз — указать сервер и запомнить адрес
python build_client.py --server-url https://example.com:443/ --save-server-url
# дальше — просто
python build_client.py
```
## Требования
Сервер:
```bash
pip install -r requirements.txt
```
`cryptography` — подпись/проверка манифестов (Ed25519).
Клиентская сборка (Windows):
```bash
pip install -r requirements-client.txt
```
`cryptography` — то же самое, но нужно уже внутри собранного exe (он сам проверяет подпись манифестов). `Pillow` — опционален, используется `updater.py` для качественного масштабирования баннера; без него баннер всё равно отрисуется, просто грубее. `pyinstaller` — сам сборщик exe.