15 KiB
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 физически не может его получить (сервер его и не отдаёт, см. ниже).
Быстрый старт
На сервере:
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-машине для сборки клиента:
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 не подходят, каждый скрипт можно запускать по отдельности.
На сервере:
# 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:
# первый раз — указать сервер и запомнить адрес
python build_client.py --server-url https://example.com:443/ --save-server-url
# дальше — просто
python build_client.py
Требования
Сервер:
pip install -r requirements.txt
cryptography — подпись/проверка манифестов (Ed25519).
Клиентская сборка (Windows):
pip install -r requirements-client.txt
cryptography — то же самое, но нужно уже внутри собранного exe (он сам проверяет подпись манифестов). Pillow — опционален, используется updater.py для качественного масштабирования баннера; без него баннер всё равно отрисуется, просто грубее. pyinstaller — сам сборщик exe.