v2.5.0: add legacy state migration

This commit is contained in:
Виталий Литвинов
2026-04-24 18:58:52 +03:00
parent 7afeb59261
commit ed9073f28f
7 changed files with 288 additions and 4 deletions

View File

@@ -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: как обновить один файл и запушить

View File

@@ -142,6 +142,8 @@ CLI и бот переведены на русский и английский.
Bootstrap.sh умеет сам обновлять всё, если запустить его повторно.
Начиная с **2.5.0** первый запуск новой версии делает автоматическую миграцию старого состояния: создаёт pre-upgrade архив в `/opt/gotelegram/backups/`, вытаскивает все ключи из `[access.users]`, сохраняет домен, порт, режим, язык, историю статистики и фактически развёрнутый сайт. Если старый `template_id` был неправильным или отсутствовал, сайт помечается как `deployed_site`, чтобы Telegram-бот не показывал первый установленный шаблон вместо текущего.
---
## 9. Удаление

View File

@@ -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", "")

View File

@@ -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

View File

@@ -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

View File

@@ -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}"

View File

@@ -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 "Шаблон сайта обновлён"
}