#!/usr/bin/env bash # Update an installed server from an unpacked release. Keeps .env, team and venv. set -Eeuo pipefail source_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" target_dir="${1:-/root/RFB}" service_name="rfb-data.service" systemd_root="${RFB_SYSTEMD_ROOT:-/etc/systemd/system}" dropin="$systemd_root/$service_name.d/20-rfb-runtime.conf" check_only="${2:-}" files=(get_data.py rfb_polling.py rfb_runtime.py rfb_logs.html rfb_version.json polling_config.json deploy.sh README_RU.txt update.sh) python_bin="$target_dir/.venv/bin/python" if [[ ! -x "$python_bin" ]]; then python_bin="$(command -v python3)"; fi "$python_bin" - "$source_dir" <<'PY' from pathlib import Path import hashlib, json, sys root = Path(sys.argv[1]); version = json.loads((root/'rfb_version.json').read_text()) for name, expected in version['files'].items(): if hashlib.sha256((root/name).read_bytes()).hexdigest() != expected: raise SystemExit('Release checksum mismatch: '+name) for name in ('get_data.py','rfb_polling.py','rfb_runtime.py'): compile((root/name).read_bytes(),str(root/name),'exec') print('RFB '+version['version']+': release validated') PY if [[ "$check_only" == "--check" ]]; then exit 0; fi if [[ "$source_dir" == "$(realpath -m "$target_dir")" ]]; then echo 'Распакуйте архив в отдельную папку, затем запустите update.sh с путём рабочего проекта.' >&2 exit 1 fi if [[ ! -d "$target_dir" ]]; then echo "Не найден установленный проект: $target_dir" >&2; exit 1; fi "$python_bin" -c 'import requests, fastapi, uvicorn, pandas, PIL, dotenv, nasio' backup_dir="${target_dir}-backup-$(date +%Y%m%d-%H%M%S)" mkdir -p -- "$backup_dir" for name in "${files[@]}" .rfb_deployment.json; do if [[ -f "$target_dir/$name" ]]; then cp -a -- "$target_dir/$name" "$backup_dir/$name"; fi done if [[ -f "$dropin" ]]; then cp -a -- "$dropin" "$backup_dir/systemd-rfb-runtime.conf"; fi was_active=0 if systemctl is-active --quiet "$service_name"; then was_active=1; fi changed=0 rollback() { local result=$? trap - ERR if [[ "$changed" == 1 ]]; then echo "Обновление не завершилось. Восстанавливаю файлы из $backup_dir" >&2 systemctl stop "$service_name" || true mkdir -p -- "$backup_dir/failed-update" for name in "${files[@]}" .rfb_deployment.json; do if [[ -f "$backup_dir/$name" ]]; then cp -a -- "$backup_dir/$name" "$target_dir/$name" elif [[ -f "$target_dir/$name" ]]; then mv -- "$target_dir/$name" "$backup_dir/failed-update/$name" fi done if [[ -f "$backup_dir/systemd-rfb-runtime.conf" ]]; then cp -a -- "$backup_dir/systemd-rfb-runtime.conf" "$dropin" elif [[ -f "$dropin" ]]; then mv -- "$dropin" "$backup_dir/failed-update/systemd-rfb-runtime.conf" fi systemctl daemon-reload || true if [[ "$was_active" == 1 ]]; then systemctl start "$service_name" || true; fi fi exit "$result" } trap rollback ERR systemctl stop "$service_name" changed=1 for name in "${files[@]}"; do if [[ "$name" != "polling_config.json" ]]; then cp -- "$source_dir/$name" "$target_dir/$name"; fi done "$python_bin" - "$source_dir" "$target_dir" <<'PY' from pathlib import Path import json, sys source, target = map(Path, sys.argv[1:]) defaults = json.loads((source/'polling_config.json').read_text()) path = target/'polling_config.json' previous = json.loads(path.read_text()) if path.exists() else {} if not isinstance(previous,dict): raise SystemExit('polling_config.json must contain an object') # Keep user-specific budgets and intervals. Migrate the old default preparation window. if previous.get('prepare_minutes') == 70: previous['prepare_minutes'] = 90 defaults.update(previous) path.write_text(json.dumps(defaults,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') PY chmod +x "$target_dir/update.sh" "$target_dir/deploy.sh" "$python_bin" "$target_dir/rfb_runtime.py" --mark-deployed "$target_dir" mkdir -p -- "$(dirname -- "$dropin")" cat > "$dropin" <<'UNIT' [Unit] After=network-online.target Wants=network-online.target StartLimitIntervalSec=300 StartLimitBurst=5 [Service] Environment=PYTHONUNBUFFERED=1 UNIT systemctl daemon-reload systemctl start "$service_name" # HTTP readiness does not depend on the upstream API or Telegram being reachable. "$python_bin" - "$source_dir/rfb_version.json" <<'PY' import json, sys, time, urllib.request expected = json.load(open(sys.argv[1]))['version'] last = None for _ in range(30): try: with urllib.request.urlopen('http://127.0.0.1:8000/api_health',timeout=2) as response: if response.status == 200 and json.load(response).get("version") == expected: break except Exception as ex: last = ex time.sleep(1) else: raise SystemExit('Сервис не ответил после обновления: '+str(last)) PY trap - ERR echo "Обновление установлено. Резервная копия: $backup_dir" echo 'Диагностика: /logs; состояние доставки Telegram и тест доступны на странице.'