From ed9073f28ff2d98d0fd748ccbd1cc65eda2b20a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B8?= =?UTF-8?q?=D1=82=D0=B2=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Fri, 24 Apr 2026 18:58:52 +0300 Subject: [PATCH] v2.5.0: add legacy state migration --- DOCS_AI.md | 15 ++++ DOCS_HUMAN.md | 2 + gotelegram-bot/bot.py | 2 + install.sh | 192 ++++++++++++++++++++++++++++++++++++++++++ lib/backup.sh | 4 +- lib/telemt_config.sh | 66 +++++++++++++++ lib/website.sh | 11 ++- 7 files changed, 288 insertions(+), 4 deletions(-) diff --git a/DOCS_AI.md b/DOCS_AI.md index 3460874..15cb5b1 100644 --- a/DOCS_AI.md +++ b/DOCS_AI.md @@ -437,6 +437,21 @@ switch_language ru|en `restore_backup` разворачивает архив обратно, перезапускает telemt и nginx. +### 13.1 Upgrade migration (v2.5.0) + +`install.sh` вызывает `auto_migrate_legacy_state` перед интерактивным меню и перед non-interactive `--action=...` для бота. Цель — комфортно обновляться даже с кривой старой логики без переустановки прокси: + +- перед изменениями создаётся `/opt/gotelegram/backups/preupgrade_2.5.0_*.tar.gz`; +- из старого `/etc/telemt/config.toml` вытаскиваются все строки `[access.users]`, а не только `main`; +- если `main` отсутствует, он добавляется с первым найденным секретом, чтобы старые ссылки не потерялись; +- сохраняются порт, `tls_domain`, `mask_port`, режим Lite/Pro, домен, язык, `stats_enabled`; +- старый telemt TOML нормализуется под v2.5.0 с `[server.api]` и metrics, затем в него возвращается полный users block; +- если сайт уже развёрнут, но template id неизвестен или старый `config.json` врёт, в `/var/www/gotelegram-site/.gotelegram_template_id` пишется `deployed_site`; +- `config.json` переписывается в актуальный формат, но с сохранением `installed_at` и пользовательских настроек; +- если telemt был активен и TOML был изменён, сервис перезапускается один раз. + +Инвариант: миграция должна быть идемпотентной. Маркер `/opt/gotelegram/.migrated_2.5.0` предотвращает повторную нормализацию без причины. + --- ## 14. Checklist: как обновить один файл и запушить diff --git a/DOCS_HUMAN.md b/DOCS_HUMAN.md index c982040..0928378 100644 --- a/DOCS_HUMAN.md +++ b/DOCS_HUMAN.md @@ -142,6 +142,8 @@ CLI и бот переведены на русский и английский. Bootstrap.sh умеет сам обновлять всё, если запустить его повторно. +Начиная с **2.5.0** первый запуск новой версии делает автоматическую миграцию старого состояния: создаёт pre-upgrade архив в `/opt/gotelegram/backups/`, вытаскивает все ключи из `[access.users]`, сохраняет домен, порт, режим, язык, историю статистики и фактически развёрнутый сайт. Если старый `template_id` был неправильным или отсутствовал, сайт помечается как `deployed_site`, чтобы Telegram-бот не показывал первый установленный шаблон вместо текущего. + --- ## 9. Удаление diff --git a/gotelegram-bot/bot.py b/gotelegram-bot/bot.py index c92b613..fe578a5 100644 --- a/gotelegram-bot/bot.py +++ b/gotelegram-bot/bot.py @@ -348,6 +348,8 @@ def template_display_name(template_id: str) -> str: """Resolve a template id to a human-friendly name from catalog/config.""" if not template_id: return "" + if template_id in ("deployed_site", "existing_site"): + return "Existing deployed site" if template_id.startswith("custom_"): config = load_json(GOTELEGRAM_CONFIG) or {} source = config.get("template_source", "") diff --git a/install.sh b/install.sh index fad9459..ed81f34 100755 --- a/install.sh +++ b/install.sh @@ -245,6 +245,195 @@ menu_version() { echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}" } +# ── Upgrade migration ──────────────────────────────────────────────────────── +snapshot_preupgrade_state() { + local marker="$GOTELEGRAM_DIR/.preupgrade_${GOTELEGRAM_VERSION}_done" + [ -f "$marker" ] && return 0 + mkdir -p "$BACKUP_DIR" + + local ts tmp archive + ts=$(date +%Y%m%d_%H%M%S) + tmp="/tmp/gotelegram_preupgrade_${ts}" + archive="$BACKUP_DIR/preupgrade_${GOTELEGRAM_VERSION}_${ts}.tar.gz" + mkdir -p "$tmp" + + [ -f "$GOTELEGRAM_CONFIG" ] && mkdir -p "$tmp/opt/gotelegram" && cp "$GOTELEGRAM_CONFIG" "$tmp/opt/gotelegram/config.json" 2>/dev/null + [ -f "$TELEMT_CONFIG" ] && mkdir -p "$tmp/etc/telemt" && cp "$TELEMT_CONFIG" "$tmp/etc/telemt/config.toml" 2>/dev/null + [ -f "$NGINX_SITE_CONF" ] && mkdir -p "$tmp/etc/nginx/sites-available" && cp "$NGINX_SITE_CONF" "$tmp/etc/nginx/sites-available/gotelegram" 2>/dev/null + [ -d "$WEBSITE_ROOT" ] && mkdir -p "$tmp/var/www/gotelegram-site" && cp -a "$WEBSITE_ROOT/." "$tmp/var/www/gotelegram-site/" 2>/dev/null + [ -f "$BOT_DIR/.env" ] && mkdir -p "$tmp/opt/gotelegram-bot" && cp "$BOT_DIR/.env" "$tmp/opt/gotelegram-bot/.env" 2>/dev/null + + if tar czf "$archive" -C "$tmp" . 2>/dev/null; then + log_dim "Pre-upgrade snapshot: $archive" + touch "$marker" 2>/dev/null || true + fi + rm -rf "$tmp" +} + +read_config_or_default() { + local key="$1" fallback="$2" + config_get "$key" 2>/dev/null || echo "$fallback" +} + +detect_deployed_template_id() { + local tpl="" + if [ -f "$WEBSITE_ROOT/.gotelegram_template_id" ]; then + tpl=$(head -1 "$WEBSITE_ROOT/.gotelegram_template_id" 2>/dev/null || echo "") + [ -n "$tpl" ] && { echo "$tpl"; return 0; } + fi + if [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ]; then + echo "deployed_site" + return 0 + fi + tpl=$(read_config_or_default template_id "") + [ -n "$tpl" ] && { echo "$tpl"; return 0; } + echo "" +} + +detect_template_source() { + local src + if [ -f "$WEBSITE_ROOT/.gotelegram_template_source" ]; then + src=$(head -1 "$WEBSITE_ROOT/.gotelegram_template_source" 2>/dev/null || echo "") + [ -n "$src" ] && { echo "$src"; return 0; } + fi + [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ] && return 0 + read_config_or_default template_source "" +} + +write_normalized_gotelegram_config() { + local mode="$1" port="$2" secret="$3" mask_host="$4" domain="$5" tpl_id="$6" tpl_source="$7" + local lang installed_at stats_enabled tmp + lang=$(read_config_or_default language "$(get_language 2>/dev/null || echo en)") + installed_at=$(read_config_or_default installed_at "$(date -Iseconds)") + stats_enabled=$(read_config_or_default stats_enabled "") + tmp=$(mktemp) || return 1 + + jq -n \ + --arg version "$GOTELEGRAM_VERSION" \ + --arg engine "telemt" \ + --arg mode "$mode" \ + --argjson port "$port" \ + --arg secret "$secret" \ + --arg mask_host "$mask_host" \ + --arg domain "$domain" \ + --arg template_id "$tpl_id" \ + --arg template_source "$tpl_source" \ + --arg language "$lang" \ + --arg installed_at "$installed_at" \ + --arg updated_at "$(date -Iseconds)" \ + --arg stats_enabled "$stats_enabled" \ + '{ + version: $version, + engine: $engine, + mode: $mode, + port: $port, + secret: $secret, + mask_host: $mask_host, + domain: $domain, + template_id: $template_id, + language: $language, + installed_at: $installed_at, + updated_at: $updated_at + } + + (if $template_source != "" then {template_source: $template_source} else {} end) + + (if $stats_enabled == "true" then {stats_enabled: true} elif $stats_enabled == "false" then {stats_enabled: false} else {} end)' \ + > "$tmp" || { rm -f "$tmp"; return 1; } + + mkdir -p "$(dirname "$GOTELEGRAM_CONFIG")" + mv "$tmp" "$GOTELEGRAM_CONFIG" + chmod 600 "$GOTELEGRAM_CONFIG" +} + +auto_migrate_legacy_state() { + local marker="$GOTELEGRAM_DIR/.migrated_${GOTELEGRAM_VERSION}" + local current_version + current_version=$(read_config_or_default version "") + if [ -f "$marker" ] && [ "$current_version" = "$GOTELEGRAM_VERSION" ]; then + return 0 + fi + + [ -f "$TELEMT_CONFIG" ] || [ -f "$GOTELEGRAM_CONFIG" ] || [ -d "$WEBSITE_ROOT" ] || return 0 + + log_step "Миграция состояния GoTelegram" + snapshot_preupgrade_state + + local mode port secret mask_host domain mask_port tpl_id tpl_source users_block tls_emulation changed=0 users_block_needs_write=0 + users_block=$(get_telemt_users_block "$TELEMT_CONFIG" 2>/dev/null || true) + secret=$(get_config_value secret "$TELEMT_CONFIG" 2>/dev/null || echo "") + [ -z "$secret" ] && secret=$(read_config_or_default secret "") + [ -z "$secret" ] && secret=$(first_telemt_user_secret "$TELEMT_CONFIG" 2>/dev/null || echo "") + [ -z "$secret" ] && secret=$(generate_hex 32) + + if [ -n "$users_block" ] && ! printf '%s\n' "$users_block" | grep -qE '^[[:space:]]*main[[:space:]]*='; then + users_block=$(printf 'main = "%s"\n%s\n' "$secret" "$users_block") + users_block_needs_write=1 + fi + if [ -z "$users_block" ]; then + users_block="main = \"$secret\"" + users_block_needs_write=1 + fi + + port=$(get_config_value port "$TELEMT_CONFIG" 2>/dev/null || echo "") + [ -z "$port" ] && port=$(read_config_or_default port "443") + [[ "$port" =~ ^[0-9]+$ ]] || port=443 + + mask_host=$(get_config_value mask_host "$TELEMT_CONFIG" 2>/dev/null || echo "") + [ -z "$mask_host" ] && mask_host=$(read_config_or_default mask_host "google.com") + domain=$(read_config_or_default domain "") + mask_port=$(get_config_value mask_port "$TELEMT_CONFIG" 2>/dev/null || echo "") + [ -z "$mask_port" ] && mask_port="443" + tls_emulation=$(toml_bool_value censorship tls_emulation "$TELEMT_CONFIG" 2>/dev/null || echo "") + + mode=$(read_config_or_default mode "") + if [ -z "$mode" ]; then + if [ -n "$domain" ] || [ "$tls_emulation" = "false" ] || grep -q 'dns_overrides' "$TELEMT_CONFIG" 2>/dev/null; then + mode="pro" + else + mode="lite" + fi + fi + if [ "$mode" = "pro" ]; then + [ -z "$domain" ] && domain="$mask_host" + [ -n "$domain" ] && mask_host="$domain" + [ "$mask_port" = "443" ] && mask_port="8443" + else + domain="" + mask_port="443" + fi + + tpl_id=$(detect_deployed_template_id) + tpl_source=$(detect_template_source || echo "") + if [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ] && [ -n "$tpl_id" ]; then + echo "$tpl_id" > "$WEBSITE_ROOT/.gotelegram_template_id" 2>/dev/null || true + [ -n "$tpl_source" ] && echo "$tpl_source" > "$WEBSITE_ROOT/.gotelegram_template_source" 2>/dev/null || true + fi + + if [ -f "$TELEMT_CONFIG" ]; then + if ! grep -q '\[server.api\]' "$TELEMT_CONFIG" 2>/dev/null || \ + ! grep -q 'metrics_listen' "$TELEMT_CONFIG" 2>/dev/null || \ + ! grep -q "GoTelegram v${GOTELEGRAM_VERSION}" "$TELEMT_CONFIG" 2>/dev/null; then + generate_telemt_toml "$secret" "$port" "$mode" "$mask_host" "$mask_port" "$TELEMT_CONFIG" >&2 + replace_telemt_users_block "$users_block" "$TELEMT_CONFIG" + changed=1 + users_block_needs_write=0 + elif [ "$users_block_needs_write" = "1" ]; then + replace_telemt_users_block "$users_block" "$TELEMT_CONFIG" + changed=1 + fi + fi + + write_normalized_gotelegram_config "$mode" "$port" "$secret" "$mask_host" "$domain" "$tpl_id" "$tpl_source" || \ + log_warning "Не удалось нормализовать config.json" + + if [ "$changed" = "1" ] && systemctl is-active --quiet "$TELEMT_SERVICE" 2>/dev/null; then + log_info "Перезапускаю telemt, чтобы применить нормализованный конфиг..." + restart_telemt || log_warning "telemt не перезапустился после миграции; проверьте journalctl -u telemt" + fi + + touch "$marker" 2>/dev/null || true + log_success "Миграция завершена: ключи, режим, домен и сайт сохранены" +} + # ── Install: mode selection ───────────────────────────────────────────────── menu_install() { # Check for v1 @@ -1491,6 +1680,7 @@ main() { if ! check_deps_present; then ensure_deps >&2 || exit 1 fi + auto_migrate_legacy_state >&2 || true bot_action_dispatch "$@" exit $? fi @@ -1509,6 +1699,8 @@ main() { } fi + auto_migrate_legacy_state || true + # First-run language picker (before banner so banner appears in chosen lang) first_run_language_picker diff --git a/lib/backup.sh b/lib/backup.sh index 2b44667..abb4dc6 100755 --- a/lib/backup.sh +++ b/lib/backup.sh @@ -51,7 +51,7 @@ create_backup() { # Шаблон сайта (если есть) if [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ]; then mkdir -p "$tmp_dir/site" - cp -r "$WEBSITE_ROOT"/* "$tmp_dir/site/" + cp -a "$WEBSITE_ROOT/." "$tmp_dir/site/" log_dim "$(_t_or backup_site_included 'Шаблон сайта включён')" fi @@ -277,7 +277,7 @@ restore_backup() { # Восстанавливаем шаблон сайта if [ -d "$backup_dir/site" ]; then mkdir -p "$WEBSITE_ROOT" - cp -r "$backup_dir/site"/* "$WEBSITE_ROOT/" + cp -a "$backup_dir/site/." "$WEBSITE_ROOT/" chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null log_success "$(_t_or backup_restored_site 'Шаблон сайта восстановлен')" fi diff --git a/lib/telemt_config.sh b/lib/telemt_config.sh index 6ae502c..6aaa55d 100755 --- a/lib/telemt_config.sh +++ b/lib/telemt_config.sh @@ -184,6 +184,72 @@ get_config_value() { esac } +get_telemt_users_block() { + local config="${1:-$TELEMT_CONFIG}" + [ -f "$config" ] || return 1 + awk ' + /^\[access\.users\]/ { in_users=1; next } + /^\[/ && in_users { exit } + in_users && /^[[:space:]]*[^#[:space:]][^=]*=/ { print } + ' "$config" +} + +first_telemt_user_secret() { + local config="${1:-$TELEMT_CONFIG}" + get_telemt_users_block "$config" | head -1 | sed 's/^[^=]*=[[:space:]]*//; s/^"//; s/".*$//' | tr -d ' ' +} + +replace_telemt_users_block() { + local users_block="$1" + local config="${2:-$TELEMT_CONFIG}" + [ -f "$config" ] || return 1 + [ -n "$users_block" ] || return 0 + + local tmp + tmp=$(mktemp) || return 1 + awk -v users="$users_block" ' + BEGIN { split(users, lines, "\n") } + /^\[access\.users\]/ { + found=1 + print + for (i = 1; i in lines; i++) { + if (lines[i] != "") print lines[i] + } + in_users=1 + next + } + /^\[/ && in_users { in_users=0 } + in_users { next } + { print } + END { + if (!found) { + print "" + print "[access.users]" + for (i = 1; i in lines; i++) { + if (lines[i] != "") print lines[i] + } + } + } + ' "$config" > "$tmp" && mv "$tmp" "$config" + chmod 600 "$config" +} + +toml_bool_value() { + local table="$1" + local key="$2" + local config="${3:-$TELEMT_CONFIG}" + awk -v table="$table" -v key="$key" ' + $0 == "[" table "]" { in_table=1; next } + /^\[/ && in_table { exit } + in_table && $1 == key { + sub(/^[^=]*=[[:space:]]*/, "") + gsub(/[[:space:]]/, "") + print + exit + } + ' "$config" +} + # ── Валидация конфига ──────────────────────────────────────────────────────── validate_telemt_config() { local config="${1:-$TELEMT_CONFIG}" diff --git a/lib/website.sh b/lib/website.sh index 788386c..c6e63bb 100755 --- a/lib/website.sh +++ b/lib/website.sh @@ -237,11 +237,15 @@ get_ssl_expiry() { # ── Деплой шаблона сайта ───────────────────────────────────────────────────── deploy_template_to_nginx() { local template_dir="$1" + local template_id="${2:-}" + local source_url="" if [ ! -d "$template_dir" ] || [ ! -f "$template_dir/index.html" ]; then log_error "Шаблон не содержит index.html: $template_dir" return 1 fi + [ -z "$template_id" ] && template_id=$(basename "$template_dir") + [ -f "$template_dir/.custom_git_source" ] && source_url=$(head -1 "$template_dir/.custom_git_source" 2>/dev/null || echo "") # Бекапим старый сайт if [ -d "$WEBSITE_ROOT" ] && [ "$(ls -A "$WEBSITE_ROOT" 2>/dev/null)" ]; then @@ -251,7 +255,10 @@ deploy_template_to_nginx() { fi mkdir -p "$WEBSITE_ROOT" - cp -r "$template_dir"/* "$WEBSITE_ROOT/" + cp -a "$template_dir/." "$WEBSITE_ROOT/" + rm -f "$WEBSITE_ROOT/.custom_git_source" 2>/dev/null || true + echo "$template_id" > "$WEBSITE_ROOT/.gotelegram_template_id" 2>/dev/null || true + [ -n "$source_url" ] && echo "$source_url" > "$WEBSITE_ROOT/.gotelegram_template_source" 2>/dev/null || true chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null || chown -R nginx:nginx "$WEBSITE_ROOT" 2>/dev/null chmod -R 755 "$WEBSITE_ROOT" @@ -334,7 +341,7 @@ remove_pro_mode() { # ── Смена шаблона ──────────────────────────────────────────────────────────── switch_template() { local new_template_dir="$1" - deploy_template_to_nginx "$new_template_dir" + deploy_template_to_nginx "$new_template_dir" "$(basename "$new_template_dir")" # nginx не требует перезапуска — статика обновилась на месте log_success "Шаблон сайта обновлён" }