v2.4.9: UBF v2.0 backup + manual secret recovery

- lib/backup.sh: complete rewrite for Unified Backup Format v2.0
  * metadata.json with backup_id (GT-YYMMDD-<last6hex>) and SHA-256 fingerprint
  * secrets.json with raw_secret, faketls_secret, proxy_link, bot_token
  * structured dirs: telemt/, gotelegram/, nginx/, letsencrypt/, site/, bot/
  * auto-detect and auto-migrate v1.1 -> v2.0 on restore
  * parse_manual_secret: accepts tg://proxy URL, ee-prefix, or raw 32-hex
  * manual_secret_input: interactive entry with env var export
- install.sh: new 3-option menu_install (new / restore / existing key)
  * install_lite_mode + install_pro_mode respect GOTELEGRAM_EXISTING_* env vars
- lib/lang/ru.sh + en.sh: v2.4.9 i18n strings (backup_*, manual_secret_*, install_menu_*)
- lib/common.sh + gotelegram-bot/bot.py: version bump to 2.4.9
This commit is contained in:
anten-ka
2026-04-12 00:07:03 +03:00
parent 9d9d12e150
commit 0e38c2b5b6
6 changed files with 605 additions and 119 deletions

View File

@@ -100,7 +100,7 @@ logger = logging.getLogger(__name__)
# CONFIGURATION # CONFIGURATION
# ============================================================================ # ============================================================================
GOTELEGRAM_VERSION = "2.4.8" GOTELEGRAM_VERSION = "2.4.9"
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json" GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
TELEMT_CONFIG = "/etc/telemt/config.toml" TELEMT_CONFIG = "/etc/telemt/config.toml"
TELEMT_SERVICE = "telemt" TELEMT_SERVICE = "telemt"

View File

@@ -258,6 +258,49 @@ menu_install() {
fi fi
fi fi
# Always start from a clean slate — any leftover env vars from a previous
# manual-key entry must not leak into a "new install" flow.
unset GOTELEGRAM_EXISTING_SECRET GOTELEGRAM_EXISTING_DOMAIN GOTELEGRAM_EXISTING_PORT
# ── Step 1: install source picker ────────────────────────────────────────
echo ""
echo -e " ${BOLD}${WHITE}$(_t_or install_source_title 'Источник установки')${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
echo -e " ${CYAN}1)${NC} ${GREEN}$(_t_or install_menu_new 'Новая установка')${NC}"
echo -e " ${DIM}$(_t_or install_menu_new_desc 'Сгенерировать новый ключ и настроить с нуля')${NC}"
echo ""
echo -e " ${CYAN}2)${NC} ${BLUE}$(_t_or install_menu_restore 'Восстановить из бекапа')${NC}"
echo -e " ${DIM}$(_t_or install_menu_restore_desc 'Полное восстановление из файла .tar.gz[.enc]')${NC}"
echo ""
echo -e " ${CYAN}3)${NC} ${YELLOW}$(_t_or install_menu_existing_key 'Использовать существующий ключ')${NC}"
echo -e " ${DIM}$(_t_or install_menu_existing_key_desc 'Ввести ссылку tg://proxy или ключ вручную')${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
echo -ne " ${WHITE}$(_t_or install_source_choice 'Выберите источник')${NC} "
read -r src_choice
src_choice="${src_choice:-}"
case "$src_choice" in
1) : ;; # fall through to mode picker
2)
if type interactive_restore &>/dev/null; then
interactive_restore
else
log_error "backup.sh not loaded"
fi
return
;;
3)
if type manual_secret_input &>/dev/null; then
manual_secret_input || return
else
log_error "backup.sh not loaded"
return
fi
;;
*) log_error "$(tf install_bad_choice "${src_choice:-<empty>}")" ; return ;;
esac
# ── Step 2: lite/pro mode picker ─────────────────────────────────────────
echo "" echo ""
echo -e " ${BOLD}${WHITE}$(t install_select_mode)${NC}" echo -e " ${BOLD}${WHITE}$(t install_select_mode)${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
@@ -274,11 +317,19 @@ menu_install() {
read -r mode_choice read -r mode_choice
mode_choice="${mode_choice:-}" mode_choice="${mode_choice:-}"
# If user provided an ee-prefixed key with a domain, hint at pro mode
if [ -n "${GOTELEGRAM_EXISTING_DOMAIN:-}" ] && [ "$mode_choice" = "1" ]; then
log_warning "$(_t_or install_hint_pro_mode 'Ключ содержит домен — обычно это Pro режим')"
fi
case "$mode_choice" in case "$mode_choice" in
1) install_lite_mode ;; 1) install_lite_mode ;;
2) install_pro_mode ;; 2) install_pro_mode ;;
*) log_error "$(tf install_bad_choice "${mode_choice:-<empty>}")" ;; *) log_error "$(tf install_bad_choice "${mode_choice:-<empty>}")" ;;
esac esac
# Clean up env vars after install, just in case
unset GOTELEGRAM_EXISTING_SECRET GOTELEGRAM_EXISTING_DOMAIN GOTELEGRAM_EXISTING_PORT
} }
# ── Lite mode ─────────────────────────────────────────────────────────────── # ── Lite mode ───────────────────────────────────────────────────────────────
@@ -290,10 +341,15 @@ install_lite_mode() {
domain=$(select_quick_domain) domain=$(select_quick_domain)
[ $? -ne 0 ] && return [ $? -ne 0 ] && return
# Port selection # Port selection — if user provided a port via existing-key flow, reuse it
local port local port
port=$(select_port) if [ -n "${GOTELEGRAM_EXISTING_PORT:-}" ] && [[ "$GOTELEGRAM_EXISTING_PORT" =~ ^[0-9]+$ ]]; then
[ $? -ne 0 ] && return port="$GOTELEGRAM_EXISTING_PORT"
log_info "$(_t_or install_reuse_port 'Используется порт из ключа'): ${port}"
else
port=$(select_port)
[ $? -ne 0 ] && return
fi
# Preflight: port conflict check (checks the external port only for lite) # Preflight: port conflict check (checks the external port only for lite)
if ! preflight_check "lite" "$port"; then if ! preflight_check "lite" "$port"; then
@@ -301,9 +357,14 @@ install_lite_mode() {
return return
fi fi
# Generate secret # Secret: reuse if provided via manual_secret_input, otherwise generate new
local secret local secret
secret=$(generate_hex 32) if [ -n "${GOTELEGRAM_EXISTING_SECRET:-}" ]; then
secret="$GOTELEGRAM_EXISTING_SECRET"
log_info "$(_t_or install_reuse_secret 'Используется переданный ключ'): ${secret:0:8}...${secret: -4}"
else
secret=$(generate_hex 32)
fi
# Confirm # Confirm
local ip local ip
@@ -354,10 +415,16 @@ install_pro_mode() {
return return
fi fi
# Enter domain # Enter domain — if provided via existing-key flow, reuse it
echo "" local user_domain=""
echo -ne " ${WHITE}$(t install_enter_domain)${NC} " if [ -n "${GOTELEGRAM_EXISTING_DOMAIN:-}" ]; then
read -r user_domain user_domain="$GOTELEGRAM_EXISTING_DOMAIN"
log_info "$(_t_or install_reuse_domain 'Используется домен из ключа'): ${user_domain}"
else
echo ""
echo -ne " ${WHITE}$(t install_enter_domain)${NC} "
read -r user_domain
fi
if [ -z "$user_domain" ] || ! validate_domain "$user_domain"; then if [ -z "$user_domain" ] || ! validate_domain "$user_domain"; then
log_error "$(tf install_bad_domain "${user_domain:-<empty>}")" log_error "$(tf install_bad_domain "${user_domain:-<empty>}")"
@@ -399,8 +466,14 @@ install_pro_mode() {
# Generate fake-TLS secret (ee + secret + hex domain) # Generate fake-TLS secret (ee + secret + hex domain)
# ee prefix tells Telegram client to masquerade traffic as TLS to domain # ee prefix tells Telegram client to masquerade traffic as TLS to domain
# Reuse existing secret if manual_secret_input provided it
local raw_secret local raw_secret
raw_secret=$(generate_hex 32) if [ -n "${GOTELEGRAM_EXISTING_SECRET:-}" ]; then
raw_secret="$GOTELEGRAM_EXISTING_SECRET"
log_info "$(_t_or install_reuse_secret 'Используется переданный ключ'): ${raw_secret:0:8}...${raw_secret: -4}"
else
raw_secret=$(generate_hex 32)
fi
local domain_hex local domain_hex
domain_hex=$(printf '%s' "$user_domain" | xxd -p | tr -d '\n') domain_hex=$(printf '%s' "$user_domain" | xxd -p | tr -d '\n')
local faketls_secret="ee${raw_secret}${domain_hex}" local faketls_secret="ee${raw_secret}${domain_hex}"

497
lib/backup.sh Executable file → Normal file
View File

@@ -1,71 +1,179 @@
#!/bin/bash #!/bin/bash
# GoTelegram v2.4 — backup and restore (i18n-aware) # GoTelegram v2.4.9Unified Backup Format (UBF) v2.0
#
# UBF v2.0 layout (inside the tarball):
# gotelegram_backup_YYYYMMDD_HHMMSS_<shortid>/
# ├── metadata.json # backup_id, versions, fingerprint, ...
# ├── secrets.json # raw_secret, faketls_secret, proxy_link, bot_token
# ├── telemt/config.toml
# ├── gotelegram/config.json
# ├── gotelegram/.language
# ├── nginx/site.conf
# ├── letsencrypt/
# │ ├── live/<domain>/{fullchain,privkey,chain,cert}.pem
# │ └── renewal/<domain>.conf
# ├── site/ (nginx document root)
# └── bot/.env
#
# Backup ID format: GT-YYMMDD-<last6hex-of-raw-secret>
# Archive name: gotelegram_backup_YYYYMMDD_HHMMSS_<shortid>.tar.gz[.enc]
#
# Encryption: AES-256-CBC + PBKDF2 (optional, password-based)
# Integrity: SHA-256 sidecar file (<archive>.sha256)
#
# Restore path automatically detects v1.1 (legacy) vs v2.0 layouts by reading
# metadata.json.backup_version. When restoring a v1.1 archive the script
# immediately writes a fresh v2.0 backup alongside the old one, so subsequent
# reinstalls can benefit from the new format.
# ── Создание бекапа ────────────────────────────────────────────────────────── # ── Utility: generate a backup ID from a raw secret ─────────────────────────
# Format: GT-YYMMDD-<last6hex>. Deterministic per-day per-key; easy to read.
generate_backup_id() {
local raw_secret="$1"
local date_part
date_part=$(date +%y%m%d)
local last6="000000"
if [ -n "$raw_secret" ] && [ ${#raw_secret} -ge 6 ]; then
last6="${raw_secret: -6}"
last6=$(echo "$last6" | tr 'A-F' 'a-f')
fi
echo "GT-${date_part}-${last6}"
}
# ── Utility: SHA-256 fingerprint of a raw secret ────────────────────────────
secret_fingerprint() {
local raw_secret="$1"
[ -z "$raw_secret" ] && { echo ""; return; }
printf '%s' "$raw_secret" | openssl dgst -sha256 2>/dev/null | awk '{print $NF}'
}
# ── Utility: hex-encode an ASCII string (for fake-TLS secret) ───────────────
_hex_encode() {
printf '%s' "$1" | xxd -p | tr -d '\n'
}
# ── Создание бекапа (UBF v2.0) ──────────────────────────────────────────────
create_backup() { create_backup() {
local password="$1" local password="${1:-}"
local output_dir="${2:-$BACKUP_DIR}" local output_dir="${2:-$BACKUP_DIR}"
# Pull current config (so backup_id can include the real secret)
local raw_secret domain mode engine port lang tpl_id mask_host
raw_secret=$(config_get secret 2>/dev/null || echo "")
domain=$(config_get domain 2>/dev/null || echo "")
mode=$(config_get mode 2>/dev/null || echo "unknown")
engine=$(config_get engine 2>/dev/null || echo "telemt")
port=$(config_get port 2>/dev/null || echo "443")
lang=$(type get_language &>/dev/null && get_language 2>/dev/null || echo "en")
tpl_id=$(config_get template_id 2>/dev/null || echo "")
mask_host=$(config_get mask_host 2>/dev/null || echo "")
# Sanitise port
[[ "$port" =~ ^[0-9]+$ ]] || port=443
# Backup id / short id (last 6 of secret, or random if unknown)
local backup_id short_id
backup_id=$(generate_backup_id "$raw_secret")
short_id="${backup_id##*-}"
local timestamp local timestamp
timestamp=$(date +%Y%m%d_%H%M%S) timestamp=$(date +%Y%m%d_%H%M%S)
local backup_name="gotelegram_backup_${timestamp}" local backup_name="gotelegram_backup_${timestamp}_${short_id}"
local tmp_dir="/tmp/${backup_name}" local tmp_dir="/tmp/${backup_name}"
mkdir -p "$tmp_dir" "$output_dir" mkdir -p "$tmp_dir" "$output_dir"
# Собираем файлы
log_info "$(_t_or backup_collecting 'Собираю конфигурацию...')" log_info "$(_t_or backup_collecting 'Собираю конфигурацию...')"
# telemt конфиг # ── telemt ──
if [ -f "$TELEMT_CONFIG" ]; then if [ -f "$TELEMT_CONFIG" ]; then
cp "$TELEMT_CONFIG" "$tmp_dir/config.toml" mkdir -p "$tmp_dir/telemt"
cp "$TELEMT_CONFIG" "$tmp_dir/telemt/config.toml"
fi fi
# GoTelegram конфиг # ── gotelegram ──
mkdir -p "$tmp_dir/gotelegram"
if [ -f "$GOTELEGRAM_CONFIG" ]; then if [ -f "$GOTELEGRAM_CONFIG" ]; then
cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram.json" cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram/config.json"
fi fi
# Language marker (i18n)
if [ -f "$GOTELEGRAM_DIR/.language" ]; then if [ -f "$GOTELEGRAM_DIR/.language" ]; then
cp "$GOTELEGRAM_DIR/.language" "$tmp_dir/.language" cp "$GOTELEGRAM_DIR/.language" "$tmp_dir/gotelegram/.language"
fi fi
# nginx конфиг (stealth mode) # ── nginx ──
if [ -f "$NGINX_SITE_CONF" ]; then if [ -f "$NGINX_SITE_CONF" ]; then
cp "$NGINX_SITE_CONF" "$tmp_dir/nginx.conf" mkdir -p "$tmp_dir/nginx"
cp "$NGINX_SITE_CONF" "$tmp_dir/nginx/site.conf"
fi fi
# SSL сертификаты # ── Let's Encrypt (full tree: live/<d>/*.pem + renewal/<d>.conf) ──
local domain
domain=$(config_get domain 2>/dev/null)
if [ -n "$domain" ] && [ -d "/etc/letsencrypt/live/$domain" ]; then if [ -n "$domain" ] && [ -d "/etc/letsencrypt/live/$domain" ]; then
mkdir -p "$tmp_dir/certs" mkdir -p "$tmp_dir/letsencrypt/live/$domain"
cp "/etc/letsencrypt/live/$domain/fullchain.pem" "$tmp_dir/certs/" 2>/dev/null # Follow symlinks — letsencrypt's live/ tree is symlinks into archive/
cp "/etc/letsencrypt/live/$domain/privkey.pem" "$tmp_dir/certs/" 2>/dev/null cp -L "/etc/letsencrypt/live/$domain/"*.pem "$tmp_dir/letsencrypt/live/$domain/" 2>/dev/null
log_dim "SSL сертификаты включены" if [ -f "/etc/letsencrypt/renewal/${domain}.conf" ]; then
mkdir -p "$tmp_dir/letsencrypt/renewal"
cp "/etc/letsencrypt/renewal/${domain}.conf" "$tmp_dir/letsencrypt/renewal/"
fi
log_dim "$(_t_or backup_ssl_included 'SSL-сертификаты включены (+ chain + renewal)')"
fi fi
# Шаблон сайта (если есть) # ── Website template ──
if [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ]; then if [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ]; then
mkdir -p "$tmp_dir/site" mkdir -p "$tmp_dir/site"
cp -r "$WEBSITE_ROOT"/* "$tmp_dir/site/" cp -r "$WEBSITE_ROOT"/* "$tmp_dir/site/" 2>/dev/null
log_dim "$(_t_or backup_site_included 'Шаблон сайта включён')" log_dim "$(_t_or backup_site_included 'Шаблон сайта включён')"
fi fi
# Метаданные # ── Telegram bot ──
local ip mode engine lang port domain if [ -f "$BOT_DIR/.env" ]; then
mkdir -p "$tmp_dir/bot"
cp "$BOT_DIR/.env" "$tmp_dir/bot/.env"
chmod 600 "$tmp_dir/bot/.env" 2>/dev/null
log_dim "$(_t_or backup_bot_included 'Конфиг Telegram-бота включён')"
fi
# ── secrets.json ──
local faketls_secret="" proxy_link="" bot_token=""
if [ -n "$raw_secret" ] && [ "$mode" = "pro" ] && [ -n "$domain" ]; then
faketls_secret="ee${raw_secret}$(_hex_encode "$domain")"
fi
if type generate_proxy_link &>/dev/null; then
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
proxy_link=$(generate_proxy_link "$domain" "$port" "$raw_secret" "$domain" 2>/dev/null || echo "")
elif [ -n "$raw_secret" ]; then
local ip
ip=$(get_server_ip)
proxy_link=$(generate_proxy_link "$ip" "$port" "$raw_secret" "$mask_host" 2>/dev/null || echo "")
fi
fi
if [ -f "$BOT_DIR/.env" ]; then
bot_token=$(grep -E '^BOT_TOKEN=' "$BOT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2-)
bot_token="${bot_token%\"}"
bot_token="${bot_token#\"}"
fi
cat > "$tmp_dir/secrets.json" << EOSEC
{
"version": "1",
"raw_secret": "${raw_secret}",
"faketls_secret": "${faketls_secret}",
"proxy_link": "${proxy_link}",
"bot_token": "${bot_token}",
"exported_at": "$(date -Iseconds)"
}
EOSEC
chmod 600 "$tmp_dir/secrets.json"
# ── metadata.json v2.0 ──
local ip fingerprint
ip=$(get_server_ip) ip=$(get_server_ip)
mode=$(config_get mode 2>/dev/null || echo "unknown") fingerprint=$(secret_fingerprint "$raw_secret")
engine=$(config_get engine 2>/dev/null || echo "telemt")
lang=$(type get_language &>/dev/null && get_language 2>/dev/null || echo "en")
port=$(config_get port 2>/dev/null || echo "443")
# Ensure port is numeric; fall back to 443 if garbage
[[ "$port" =~ ^[0-9]+$ ]] || port=443
domain=$(config_get domain 2>/dev/null || echo "")
cat > "$tmp_dir/metadata.json" << EOMETA cat > "$tmp_dir/metadata.json" << EOMETA
{ {
"backup_version": "1.1", "backup_version": "2.0",
"backup_id": "${backup_id}",
"gotelegram_version": "$GOTELEGRAM_VERSION", "gotelegram_version": "$GOTELEGRAM_VERSION",
"created_at": "$(date -Iseconds)", "created_at": "$(date -Iseconds)",
"hostname": "$(hostname)", "hostname": "$(hostname)",
@@ -74,34 +182,37 @@ create_backup() {
"mode": "$mode", "mode": "$mode",
"language": "$lang", "language": "$lang",
"port": $port, "port": $port,
"domain": "$domain" "domain": "$domain",
"template_id": "$tpl_id",
"mask_host": "$mask_host",
"secret_fingerprint_sha256": "$fingerprint",
"has_secrets": true,
"has_letsencrypt": $([ -d "$tmp_dir/letsencrypt" ] && echo true || echo false),
"has_site": $([ -d "$tmp_dir/site" ] && echo true || echo false),
"has_bot": $([ -d "$tmp_dir/bot" ] && echo true || echo false)
} }
EOMETA EOMETA
# Архивируем # ── Archive ──
local tar_file="/tmp/${backup_name}.tar.gz" local tar_file="/tmp/${backup_name}.tar.gz"
if ! tar czf "$tar_file" -C /tmp "$backup_name" 2>/dev/null; then if ! tar czf "$tar_file" -C /tmp "$backup_name" 2>/dev/null; then
log_error "$(_t_or backup_archive_err 'Ошибка создания архива')" log_error "$(_t_or backup_archive_err 'Ошибка создания архива')"
rm -rf "$tmp_dir" rm -rf "$tmp_dir"; rm -f "$tar_file"
rm -f "$tar_file"
return 1 return 1
fi fi
if [ ! -f "$tar_file" ]; then if [ ! -f "$tar_file" ]; then
log_error "$(_t_or backup_archive_missing 'Архив не создан')" log_error "$(_t_or backup_archive_missing 'Архив не создан')"
rm -rf "$tmp_dir" rm -rf "$tmp_dir"
return 1 return 1
fi fi
# Шифруем если задан пароль # ── Encrypt (optional) ──
local final_file="" local final_file=""
if [ -n "$password" ]; then if [ -n "$password" ]; then
final_file="${output_dir}/${backup_name}.tar.gz.enc" final_file="${output_dir}/${backup_name}.tar.gz.enc"
openssl enc -aes-256-cbc -salt -pbkdf2 -in "$tar_file" -out "$final_file" -pass "pass:${password}" 2>/dev/null if ! openssl enc -aes-256-cbc -salt -pbkdf2 -in "$tar_file" -out "$final_file" -pass "pass:${password}" 2>/dev/null; then
if [ $? -ne 0 ]; then
log_error "$(_t_or backup_encrypt_err 'Ошибка шифрования')" log_error "$(_t_or backup_encrypt_err 'Ошибка шифрования')"
rm -f "$tar_file" rm -f "$tar_file"; rm -rf "$tmp_dir"
rm -rf "$tmp_dir"
return 1 return 1
fi fi
rm -f "$tar_file" rm -f "$tar_file"
@@ -111,27 +222,36 @@ EOMETA
mv "$tar_file" "$final_file" mv "$tar_file" "$final_file"
fi fi
# SHA256 подпись # SHA-256 sidecar
sha256sum "$final_file" > "${final_file}.sha256" 2>/dev/null sha256sum "$final_file" > "${final_file}.sha256" 2>/dev/null
# Очистка # Cleanup
rm -rf "$tmp_dir" rm -rf "$tmp_dir"
local size local size
size=$(du -h "$final_file" | cut -f1) size=$(du -h "$final_file" | cut -f1)
if type tf &>/dev/null; then
log_success "$(tf backup_created_fmt "$final_file" "$size")" echo "" >&2
else echo -e " ${BOLD}${GREEN}$(_t_or backup_created 'Бекап создан')${NC}" >&2
log_success "Бекап создан: $final_file ($size)" echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
echo -e " ${WHITE}$(_t_or backup_id_label 'Backup ID'):${NC} ${BOLD}${CYAN}${backup_id}${NC}" >&2
echo -e " ${WHITE}$(_t_or backup_file_label 'Файл'):${NC} ${final_file}" >&2
echo -e " ${WHITE}$(_t_or backup_size_label 'Размер'):${NC} ${size}" >&2
if [ -n "$raw_secret" ]; then
echo -e " ${WHITE}$(_t_or backup_key_label 'Ключ в бекапе (fingerprint)'):${NC} ${DIM}${fingerprint:0:32}...${NC}" >&2
fi fi
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
echo "" >&2
# Only the final file path on stdout (callers capture it)
echo "$final_file" echo "$final_file"
return 0 return 0
} }
# ── Восстановление из бекапа ──────────────────────────────────────────────── # ── Восстановление из бекапа (auto-detect v1.1 vs v2.0) ─────────────────────
restore_backup() { restore_backup() {
local backup_file="$1" local backup_file="$1"
local password="$2" local password="${2:-}"
if [ ! -f "$backup_file" ]; then if [ ! -f "$backup_file" ]; then
if type tf &>/dev/null; then if type tf &>/dev/null; then
@@ -145,17 +265,16 @@ restore_backup() {
local tmp_dir="/tmp/gotelegram_restore_$$" local tmp_dir="/tmp/gotelegram_restore_$$"
mkdir -p "$tmp_dir" mkdir -p "$tmp_dir"
# Расшифровываем если нужно # ── Decrypt if needed ──
local tar_file="" local tar_file=""
if echo "$backup_file" | grep -q '\.enc$'; then if echo "$backup_file" | grep -q '\.enc$'; then
if [ -z "$password" ]; then if [ -z "$password" ]; then
echo -ne " $(_t_or backup_enter_pass 'Введите пароль от бекапа'): " echo -ne " $(_t_or backup_enter_pass 'Введите пароль от бекапа'): " >&2
read -rs password read -rs password
echo "" echo "" >&2
fi fi
tar_file="/tmp/gotelegram_restore_$$.tar.gz" tar_file="/tmp/gotelegram_restore_$$.tar.gz"
openssl enc -aes-256-cbc -d -pbkdf2 -in "$backup_file" -out "$tar_file" -pass "pass:${password}" 2>/dev/null if ! openssl enc -aes-256-cbc -d -pbkdf2 -in "$backup_file" -out "$tar_file" -pass "pass:${password}" 2>/dev/null; then
if [ $? -ne 0 ]; then
log_error "$(_t_or backup_bad_pass 'Неверный пароль или повреждённый файл')" log_error "$(_t_or backup_bad_pass 'Неверный пароль или повреждённый файл')"
rm -rf "$tmp_dir" "$tar_file" rm -rf "$tmp_dir" "$tar_file"
return 1 return 1
@@ -164,105 +283,155 @@ restore_backup() {
tar_file="$backup_file" tar_file="$backup_file"
fi fi
# Распаковываем # ── Extract ──
tar xzf "$tar_file" -C "$tmp_dir" 2>/dev/null if ! tar xzf "$tar_file" -C "$tmp_dir" 2>/dev/null; then
if [ $? -ne 0 ]; then
log_error "$(_t_or backup_extract_err 'Ошибка распаковки архива')" log_error "$(_t_or backup_extract_err 'Ошибка распаковки архива')"
rm -rf "$tmp_dir" rm -rf "$tmp_dir"
[ "$tar_file" != "$backup_file" ] && rm -f "$tar_file"
return 1 return 1
fi fi
# Находим папку бекапа # Find the single top-level dir inside the archive
local backup_dir local backup_dir
backup_dir=$(find "$tmp_dir" -maxdepth 1 -type d -name "gotelegram_backup_*" | head -1) backup_dir=$(find "$tmp_dir" -maxdepth 1 -mindepth 1 -type d -name "gotelegram_backup_*" | head -1)
[ -z "$backup_dir" ] && backup_dir="$tmp_dir" [ -z "$backup_dir" ] && backup_dir="$tmp_dir"
# Проверяем метаданные # ── Parse metadata.json ──
local bk_version="1.1" bk_id="" bk_mode="" bk_domain="" bk_ip="" bk_lang="" bk_date=""
if [ -f "$backup_dir/metadata.json" ]; then if [ -f "$backup_dir/metadata.json" ]; then
local bk_version bk_mode bk_ip bk_lang bk_date bk_version=$(jq -r '.backup_version // "1.1"' "$backup_dir/metadata.json")
bk_version=$(jq -r '.gotelegram_version // "unknown"' "$backup_dir/metadata.json") bk_id=$(jq -r '.backup_id // empty' "$backup_dir/metadata.json")
bk_mode=$(jq -r '.mode // "unknown"' "$backup_dir/metadata.json") bk_mode=$(jq -r '.mode // "unknown"' "$backup_dir/metadata.json")
bk_ip=$(jq -r '.ip // "unknown"' "$backup_dir/metadata.json") bk_domain=$(jq -r '.domain // empty' "$backup_dir/metadata.json")
bk_ip=$(jq -r '.ip // "-"' "$backup_dir/metadata.json")
bk_lang=$(jq -r '.language // "-"' "$backup_dir/metadata.json") bk_lang=$(jq -r '.language // "-"' "$backup_dir/metadata.json")
bk_date=$(jq -r '.created_at // "-"' "$backup_dir/metadata.json") bk_date=$(jq -r '.created_at // "-"' "$backup_dir/metadata.json")
echo ""
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_label 'Бекап'):${NC}"
echo -e " $(_t_or backup_version_label 'Версия'): $bk_version | $(_t_or backup_mode_label 'Режим'): $bk_mode | IP: $bk_ip | $(_t_or backup_lang_label 'Язык'): $bk_lang"
echo -e " $(_t_or backup_date_label 'Дата'): $bk_date"
echo ""
fi fi
echo "" >&2
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_label 'Бекап'):${NC}" >&2
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
if [ -n "$bk_id" ]; then
echo -e " ${WHITE}$(_t_or backup_id_label 'Backup ID'):${NC} ${BOLD}${CYAN}${bk_id}${NC}" >&2
fi
echo -e " ${WHITE}$(_t_or backup_format_label 'Формат'):${NC} UBF ${bk_version}" >&2
echo -e " ${WHITE}$(_t_or backup_mode_label 'Режим'):${NC} ${bk_mode}${bk_domain:+ | $bk_domain}" >&2
echo -e " ${WHITE}$(_t_or backup_lang_label 'Язык'):${NC} ${bk_lang} | IP: ${bk_ip}" >&2
echo -e " ${WHITE}$(_t_or backup_date_label 'Дата'):${NC} ${bk_date}" >&2
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
echo "" >&2
if ! confirm "$(_t_or backup_confirm_restore 'Восстановить конфигурацию? Текущие настройки будут перезаписаны.')"; then if ! confirm "$(_t_or backup_confirm_restore 'Восстановить конфигурацию? Текущие настройки будут перезаписаны.')"; then
rm -rf "$tmp_dir" rm -rf "$tmp_dir"
[ "$tar_file" != "$backup_file" ] && rm -f "$tar_file"
return 0 return 0
fi fi
# Останавливаем сервисы
stop_telemt 2>/dev/null stop_telemt 2>/dev/null
systemctl stop nginx 2>/dev/null systemctl stop nginx 2>/dev/null
# Восстанавливаем telemt конфиг # ── Detect layout ──
if [ -f "$backup_dir/config.toml" ]; then # v2.0 paths: telemt/config.toml, gotelegram/config.json, nginx/site.conf, letsencrypt/live/<d>/
# v1.1 paths: config.toml, gotelegram.json, nginx.conf, certs/
local src_telemt src_gt src_lang src_nginx src_le_live src_le_renewal src_site src_bot
if [ "$bk_version" = "2.0" ] || [ -d "$backup_dir/telemt" ]; then
src_telemt="$backup_dir/telemt/config.toml"
src_gt="$backup_dir/gotelegram/config.json"
src_lang="$backup_dir/gotelegram/.language"
src_nginx="$backup_dir/nginx/site.conf"
src_le_live=$(find "$backup_dir/letsencrypt/live" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | head -1)
src_le_renewal="$backup_dir/letsencrypt/renewal"
src_site="$backup_dir/site"
src_bot="$backup_dir/bot/.env"
else
src_telemt="$backup_dir/config.toml"
src_gt="$backup_dir/gotelegram.json"
src_lang="$backup_dir/.language"
src_nginx="$backup_dir/nginx.conf"
src_le_live="$backup_dir/certs" # v1.1 dumps certs flat
src_le_renewal=""
src_site="$backup_dir/site"
src_bot="" # v1.1 never backed up bot
fi
# ── telemt config ──
if [ -f "$src_telemt" ]; then
mkdir -p /etc/telemt mkdir -p /etc/telemt
cp "$backup_dir/config.toml" "$TELEMT_CONFIG" cp "$src_telemt" "$TELEMT_CONFIG"
chmod 600 "$TELEMT_CONFIG" chmod 600 "$TELEMT_CONFIG"
log_success "$(_t_or backup_restored_telemt 'telemt конфиг восстановлен')" log_success "$(_t_or backup_restored_telemt 'telemt конфиг восстановлен')"
fi fi
# Восстанавливаем GoTelegram конфиг # ── GoTelegram config ──
if [ -f "$backup_dir/gotelegram.json" ]; then if [ -f "$src_gt" ]; then
mkdir -p "$GOTELEGRAM_DIR" mkdir -p "$GOTELEGRAM_DIR"
cp "$backup_dir/gotelegram.json" "$GOTELEGRAM_CONFIG" cp "$src_gt" "$GOTELEGRAM_CONFIG"
log_success "$(_t_or backup_restored_gotelegram 'GoTelegram конфиг восстановлен')" log_success "$(_t_or backup_restored_gotelegram 'GoTelegram конфиг восстановлен')"
fi fi
# Восстанавливаем language marker (i18n) # ── Language ──
if [ -f "$backup_dir/.language" ]; then if [ -f "$src_lang" ]; then
mkdir -p "$GOTELEGRAM_DIR" mkdir -p "$GOTELEGRAM_DIR"
cp "$backup_dir/.language" "$GOTELEGRAM_DIR/.language" cp "$src_lang" "$GOTELEGRAM_DIR/.language"
log_success "$(_t_or backup_restored_lang 'Язык интерфейса восстановлен')" log_success "$(_t_or backup_restored_lang 'Язык интерфейса восстановлен')"
fi fi
# Восстанавливаем nginx конфиг # ── nginx ──
if [ -f "$backup_dir/nginx.conf" ]; then if [ -f "$src_nginx" ]; then
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
cp "$backup_dir/nginx.conf" "$NGINX_SITE_CONF" cp "$src_nginx" "$NGINX_SITE_CONF"
ln -sf "$NGINX_SITE_CONF" "$NGINX_SITE_LINK" ln -sf "$NGINX_SITE_CONF" "$NGINX_SITE_LINK"
log_success "$(_t_or backup_restored_nginx 'nginx конфиг восстановлен')" log_success "$(_t_or backup_restored_nginx 'nginx конфиг восстановлен')"
fi fi
# Восстанавливаем SSL # ── Let's Encrypt (v2.0: full tree; v1.1: just flat certs/) ──
if [ -d "$backup_dir/certs" ]; then if [ -n "$bk_domain" ] && [ -d "$src_le_live" ]; then
local domain local live_dir="/etc/letsencrypt/live/$bk_domain"
domain=$(config_get domain 2>/dev/null) mkdir -p "$live_dir"
if [ -n "$domain" ]; then cp "$src_le_live/"*.pem "$live_dir/" 2>/dev/null
local cert_dir="/etc/letsencrypt/live/$domain" if [ -n "$src_le_renewal" ] && [ -f "$src_le_renewal/${bk_domain}.conf" ]; then
mkdir -p "$cert_dir" mkdir -p /etc/letsencrypt/renewal
cp "$backup_dir/certs/"* "$cert_dir/" 2>/dev/null cp "$src_le_renewal/${bk_domain}.conf" "/etc/letsencrypt/renewal/"
log_success "$(_t_or backup_restored_ssl 'SSL сертификаты восстановлены')"
fi fi
log_success "$(_t_or backup_restored_ssl 'SSL сертификаты восстановлены')"
fi fi
# Восстанавливаем шаблон сайта # ── Site ──
if [ -d "$backup_dir/site" ]; then if [ -d "$src_site" ] && [ -f "$src_site/index.html" ]; then
mkdir -p "$WEBSITE_ROOT" mkdir -p "$WEBSITE_ROOT"
cp -r "$backup_dir/site"/* "$WEBSITE_ROOT/" cp -r "$src_site"/* "$WEBSITE_ROOT/"
chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null
log_success "$(_t_or backup_restored_site 'Шаблон сайта восстановлен')" log_success "$(_t_or backup_restored_site 'Шаблон сайта восстановлен')"
fi fi
# Запускаем сервисы # ── Bot .env (v2.0 only) ──
if is_telemt_installed; then if [ -n "$src_bot" ] && [ -f "$src_bot" ]; then
mkdir -p "$BOT_DIR"
cp "$src_bot" "$BOT_DIR/.env"
chmod 600 "$BOT_DIR/.env"
log_success "$(_t_or backup_restored_bot 'Конфиг Telegram-бота восстановлен')"
fi
# ── Start services ──
if type is_telemt_installed &>/dev/null && is_telemt_installed; then
start_telemt start_telemt
fi fi
systemctl start nginx 2>/dev/null systemctl start nginx 2>/dev/null
# Очистка # ── Cleanup ──
rm -rf "$tmp_dir" rm -rf "$tmp_dir"
[ "$tar_file" != "$backup_file" ] && rm -f "$tar_file" [ "$tar_file" != "$backup_file" ] && rm -f "$tar_file"
log_success "$(_t_or backup_restore_done 'Восстановление завершено!')" log_success "$(_t_or backup_restore_done 'Восстановление завершено!')"
show_proxy_info
# ── Auto-migrate v1.1 → v2.0 ──
if [ "$bk_version" != "2.0" ]; then
log_info "$(_t_or backup_automigrate 'Конвертирую старый бекап в UBF v2.0...')"
create_backup "" >/dev/null 2>&1 && \
log_success "$(_t_or backup_migrated 'Свежий UBF v2.0 бекап сохранён в $BACKUP_DIR')"
fi
type show_proxy_info &>/dev/null && show_proxy_info
return 0 return 0
} }
@@ -273,24 +442,26 @@ list_backups() {
return 1 return 1
fi fi
echo "" echo "" >&2
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_list_title 'Доступные бекапы'):${NC}" echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_list_title 'Доступные бекапы'):${NC}" >&2
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..70})${NC}" >&2
local i=1 local i=1
for f in "$BACKUP_DIR"/gotelegram_backup_*.tar.gz*; do for f in "$BACKUP_DIR"/gotelegram_backup_*.tar.gz*; do
[ -f "$f" ] || continue [ -f "$f" ] || continue
[[ "$f" == *.sha256 ]] && continue [[ "$f" == *.sha256 ]] && continue
local size date_str name local size name date_str id_tail encrypted=""
size=$(du -h "$f" | cut -f1) size=$(du -h "$f" | cut -f1)
name=$(basename "$f") name=$(basename "$f")
date_str=$(echo "$name" | grep -oE '[0-9]{8}_[0-9]{6}' | head -1) date_str=$(echo "$name" | grep -oE '[0-9]{8}_[0-9]{6}' | head -1)
local encrypted="" id_tail=$(echo "$name" | grep -oE '_[0-9a-f]{6}\.tar' | head -1 | tr -d '_.tar')
[[ "$f" == *.enc ]] && encrypted=" 🔒" [[ "$f" == *.enc ]] && encrypted=" 🔒"
echo -e " ${CYAN}${i})${NC} ${name} (${size})${encrypted}" local id_display=""
[ -n "$id_tail" ] && id_display=" ${DIM}[...${id_tail}]${NC}"
echo -e " ${CYAN}${i})${NC} ${name} (${size})${encrypted}${id_display}" >&2
((i++)) ((i++))
done done
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" echo -e " ${DIM}$(printf '─%.0s' {1..70})${NC}" >&2
} }
# ── Очистка старых бекапов ─────────────────────────────────────────────────── # ── Очистка старых бекапов ───────────────────────────────────────────────────
@@ -337,7 +508,7 @@ interactive_backup() {
fi fi
fi fi
create_backup "$password" create_backup "$password" >/dev/null
cleanup_old_backups cleanup_old_backups
} }
@@ -371,3 +542,115 @@ interactive_restore() {
restore_backup "$backup_file" restore_backup "$backup_file"
} }
# ── Manual secret recovery (v2.4.9) ──────────────────────────────────────────
# Parser accepts any of the 3 formats and emits key=value lines on stdout:
# tg://proxy?server=X&port=Y&secret=Z → raw_secret, server, port, domain (if ee-prefix)
# ee<32hex><hex_domain> → raw_secret, domain
# <32hex> → raw_secret only
# Returns 0 on success, 1 on parse failure.
parse_manual_secret() {
local input="$1"
input=$(echo "$input" | tr -d ' \t\n\r')
[ -z "$input" ] && return 1
local raw_secret="" domain="" server="" port=""
if echo "$input" | grep -q '^tg://proxy?'; then
local qs="${input#tg://proxy?}"
local kv k v
local -a kvs
IFS='&' read -ra kvs <<< "$qs"
for kv in "${kvs[@]}"; do
k="${kv%%=*}"
v="${kv#*=}"
case "$k" in
server) server="$v" ;;
port) port="$v" ;;
secret) raw_secret="$v" ;;
esac
done
[ -z "$raw_secret" ] && return 1
# Strip hex-escapes that Telegram sometimes URL-encodes
raw_secret=$(echo "$raw_secret" | tr -d '%')
fi
# After pulling from URL (if any), raw_secret might still be ee-prefixed.
# Otherwise, try the raw_secret as the whole input.
local candidate="${raw_secret:-$input}"
if [[ "$candidate" =~ ^[eE][eE][0-9a-fA-F]{32}[0-9a-fA-F]*$ ]]; then
raw_secret="${candidate:2:32}"
local hex_domain="${candidate:34}"
if [ -n "$hex_domain" ]; then
local decoded
decoded=$(echo "$hex_domain" | xxd -r -p 2>/dev/null)
# Validate decoded looks like a domain
if echo "$decoded" | grep -qE '^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; then
domain="$decoded"
fi
fi
elif [[ "$candidate" =~ ^[0-9a-fA-F]{32}$ ]]; then
raw_secret="$candidate"
else
return 1
fi
[[ "$raw_secret" =~ ^[0-9a-fA-F]{32}$ ]] || return 1
raw_secret=$(echo "$raw_secret" | tr 'A-F' 'a-f')
echo "raw_secret=$raw_secret"
[ -n "$domain" ] && echo "domain=$domain"
[ -n "$server" ] && echo "server=$server"
[ -n "$port" ] && echo "port=$port"
return 0
}
# ── Interactive: user types their old key, we parse it ─────────────────────
manual_secret_input() {
echo "" >&2
echo -e " ${BOLD}${WHITE}🔑 $(_t_or manual_secret_title 'Ввод существующего ключа')${NC}" >&2
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
echo -e " ${DIM}$(_t_or manual_secret_help1 'Поддерживаются форматы:')${NC}" >&2
echo -e " ${DIM} • tg://proxy?server=...&port=...&secret=...${NC}" >&2
echo -e " ${DIM} • ee<32hex><hexdomain> (fake-TLS)${NC}" >&2
echo -e " ${DIM} • 32hex (только raw secret)${NC}" >&2
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
echo -ne " ${WHITE}$(_t_or manual_secret_prompt 'Вставьте ключ'):${NC} " >&2
read -r user_input
if [ -z "$user_input" ]; then
log_error "$(_t_or manual_secret_empty 'Ключ не введён')"
return 1
fi
local parsed
if ! parsed=$(parse_manual_secret "$user_input"); then
log_error "$(_t_or manual_secret_bad 'Не удалось распознать формат ключа')"
return 1
fi
local p_raw="" p_domain="" p_server="" p_port=""
while IFS='=' read -r k v; do
case "$k" in
raw_secret) p_raw="$v" ;;
domain) p_domain="$v" ;;
server) p_server="$v" ;;
port) p_port="$v" ;;
esac
done <<< "$parsed"
echo "" >&2
echo -e " ${GREEN}$(_t_or manual_secret_parsed 'Ключ распознан')${NC}" >&2
echo -e " ${WHITE}raw_secret:${NC} ${DIM}${p_raw:0:8}...${p_raw: -4}${NC}" >&2
[ -n "$p_domain" ] && echo -e " ${WHITE}domain:${NC} ${CYAN}${p_domain}${NC}" >&2
[ -n "$p_server" ] && echo -e " ${WHITE}server:${NC} ${CYAN}${p_server}${NC}" >&2
[ -n "$p_port" ] && echo -e " ${WHITE}port:${NC} ${CYAN}${p_port}${NC}" >&2
echo "" >&2
# Export for the subsequent install flow to pick up
export GOTELEGRAM_EXISTING_SECRET="$p_raw"
export GOTELEGRAM_EXISTING_DOMAIN="$p_domain"
export GOTELEGRAM_EXISTING_PORT="$p_port"
return 0
}

View File

@@ -3,7 +3,7 @@
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware # Colors, logging, spinner, system helpers, v1 compat, i18n-aware
# ── Version ─────────────────────────────────────────────────────────────────── # ── Version ───────────────────────────────────────────────────────────────────
GOTELEGRAM_VERSION="2.4.8" GOTELEGRAM_VERSION="2.4.9"
GOTELEGRAM_NAME="GoTelegram" GOTELEGRAM_NAME="GoTelegram"
# ── Пути ────────────────────────────────────────────────────────────────────── # ── Пути ──────────────────────────────────────────────────────────────────────

View File

@@ -387,3 +387,68 @@ I18N[v1_migration_cancelled]="Migration cancelled. v1 left intact."
I18N[v1_stopping]="Stopping v1 container..." I18N[v1_stopping]="Stopping v1 container..."
I18N[v1_config_saved]="v1 config saved to %s" I18N[v1_config_saved]="v1 config saved to %s"
I18N[v1_port_freed]="v1 stopped. Port %s freed." I18N[v1_port_freed]="v1 stopped. Port %s freed."
# ── v2.4.9: UBF v2.0 backup + manual secret recovery ─────────────────────
I18N[install_source_title]="Installation source"
I18N[install_source_choice]="Choose source [1-3]:"
I18N[install_menu_new]="Fresh installation"
I18N[install_menu_new_desc]="Generate a new key and set up from scratch"
I18N[install_menu_restore]="Restore from backup"
I18N[install_menu_restore_desc]="Full restore from a .tar.gz[.enc] file"
I18N[install_menu_existing_key]="Use existing key"
I18N[install_menu_existing_key_desc]="Paste a tg://proxy link or a key manually"
I18N[install_hint_pro_mode]="The key contains a domain — this is usually Pro mode"
I18N[install_reuse_secret]="Using the provided key"
I18N[install_reuse_domain]="Using domain from the key"
I18N[install_reuse_port]="Using port from the key"
I18N[manual_secret_title]="Enter existing key"
I18N[manual_secret_help1]="Supported formats:"
I18N[manual_secret_prompt]="Paste the key"
I18N[manual_secret_empty]="Key is empty"
I18N[manual_secret_bad]="Could not parse the key format"
I18N[manual_secret_parsed]="Key parsed"
I18N[backup_id_label]="Backup ID"
I18N[backup_file_label]="File"
I18N[backup_size_label]="Size"
I18N[backup_key_label]="Key in backup (fingerprint)"
I18N[backup_format_label]="Format"
I18N[backup_mode_label]="Mode"
I18N[backup_lang_label]="Language"
I18N[backup_date_label]="Date"
I18N[backup_label]="Backup"
I18N[backup_ssl_included]="SSL certificates included (+ chain + renewal)"
I18N[backup_site_included]="Website template included"
I18N[backup_bot_included]="Telegram bot config included"
I18N[backup_restored_bot]="Telegram bot config restored"
I18N[backup_automigrate]="Converting legacy backup to UBF v2.0..."
I18N[backup_migrated]="Fresh UBF v2.0 backup saved"
I18N[backup_collecting]="Collecting configuration..."
I18N[backup_archive_err]="Archive creation failed"
I18N[backup_archive_missing]="Archive was not created"
I18N[backup_encrypt_err]="Encryption failed"
I18N[backup_encrypted]="Backup encrypted (AES-256-CBC)"
I18N[backup_created]="Backup created"
I18N[backup_enter_pass]="Enter password"
I18N[backup_repeat_pass]="Repeat password"
I18N[backup_pass_mismatch]="Passwords do not match"
I18N[backup_pass_short]="Password too short (min 6 chars)"
I18N[backup_bad_pass]="Wrong password or corrupted file"
I18N[backup_extract_err]="Archive extraction failed"
I18N[backup_confirm_restore]="Restore configuration? Current settings will be overwritten."
I18N[backup_restored_telemt]="telemt config restored"
I18N[backup_restored_gotelegram]="GoTelegram config restored"
I18N[backup_restored_lang]="Interface language restored"
I18N[backup_restored_nginx]="nginx config restored"
I18N[backup_restored_ssl]="SSL certificates restored"
I18N[backup_restored_site]="Website template restored"
I18N[backup_restore_done]="Restore completed!"
I18N[backup_create_title]="Create backup"
I18N[backup_encrypt_prompt]="Encrypt the backup with a password?"
I18N[backup_none]="No backups found"
I18N[backup_list_title]="Available backups"
I18N[backup_pick_prompt]="Backup number (or file path)"
I18N[backup_not_found]="Backup not found"
I18N[backup_file_not_found_fmt]="File not found: %s"
I18N[backup_cleanup_fmt]="Deleted %s old backups (kept %s)"

View File

@@ -387,3 +387,68 @@ I18N[v1_migration_cancelled]="Миграция отменена. v1 оставл
I18N[v1_stopping]="Остановка v1 контейнера..." I18N[v1_stopping]="Остановка v1 контейнера..."
I18N[v1_config_saved]="Конфиг v1 сохранён в %s" I18N[v1_config_saved]="Конфиг v1 сохранён в %s"
I18N[v1_port_freed]="v1 остановлен. Порт %s освобождён." I18N[v1_port_freed]="v1 остановлен. Порт %s освобождён."
# ── v2.4.9: UBF v2.0 backup + manual secret recovery ─────────────────────
I18N[install_source_title]="Источник установки"
I18N[install_source_choice]="Выберите источник [1-3]:"
I18N[install_menu_new]="Новая установка"
I18N[install_menu_new_desc]="Сгенерировать новый ключ и настроить с нуля"
I18N[install_menu_restore]="Восстановить из бекапа"
I18N[install_menu_restore_desc]="Полное восстановление из файла .tar.gz[.enc]"
I18N[install_menu_existing_key]="Использовать существующий ключ"
I18N[install_menu_existing_key_desc]="Ввести ссылку tg://proxy или ключ вручную"
I18N[install_hint_pro_mode]="Ключ содержит домен — обычно это Pro режим"
I18N[install_reuse_secret]="Используется переданный ключ"
I18N[install_reuse_domain]="Используется домен из ключа"
I18N[install_reuse_port]="Используется порт из ключа"
I18N[manual_secret_title]="Ввод существующего ключа"
I18N[manual_secret_help1]="Поддерживаются форматы:"
I18N[manual_secret_prompt]="Вставьте ключ"
I18N[manual_secret_empty]="Ключ не введён"
I18N[manual_secret_bad]="Не удалось распознать формат ключа"
I18N[manual_secret_parsed]="Ключ распознан"
I18N[backup_id_label]="Backup ID"
I18N[backup_file_label]="Файл"
I18N[backup_size_label]="Размер"
I18N[backup_key_label]="Ключ в бекапе (fingerprint)"
I18N[backup_format_label]="Формат"
I18N[backup_mode_label]="Режим"
I18N[backup_lang_label]="Язык"
I18N[backup_date_label]="Дата"
I18N[backup_label]="Бекап"
I18N[backup_ssl_included]="SSL-сертификаты включены (+ chain + renewal)"
I18N[backup_site_included]="Шаблон сайта включён"
I18N[backup_bot_included]="Конфиг Telegram-бота включён"
I18N[backup_restored_bot]="Конфиг Telegram-бота восстановлен"
I18N[backup_automigrate]="Конвертирую старый бекап в UBF v2.0..."
I18N[backup_migrated]="Свежий UBF v2.0 бекап сохранён"
I18N[backup_collecting]="Собираю конфигурацию..."
I18N[backup_archive_err]="Ошибка создания архива"
I18N[backup_archive_missing]="Архив не создан"
I18N[backup_encrypt_err]="Ошибка шифрования"
I18N[backup_encrypted]="Бекап зашифрован (AES-256-CBC)"
I18N[backup_created]="Бекап создан"
I18N[backup_enter_pass]="Введите пароль"
I18N[backup_repeat_pass]="Повторите пароль"
I18N[backup_pass_mismatch]="Пароли не совпадают"
I18N[backup_pass_short]="Пароль слишком короткий (минимум 6 символов)"
I18N[backup_bad_pass]="Неверный пароль или повреждённый файл"
I18N[backup_extract_err]="Ошибка распаковки архива"
I18N[backup_confirm_restore]="Восстановить конфигурацию? Текущие настройки будут перезаписаны."
I18N[backup_restored_telemt]="telemt конфиг восстановлен"
I18N[backup_restored_gotelegram]="GoTelegram конфиг восстановлен"
I18N[backup_restored_lang]="Язык интерфейса восстановлен"
I18N[backup_restored_nginx]="nginx конфиг восстановлен"
I18N[backup_restored_ssl]="SSL сертификаты восстановлены"
I18N[backup_restored_site]="Шаблон сайта восстановлен"
I18N[backup_restore_done]="Восстановление завершено!"
I18N[backup_create_title]="Создание бекапа"
I18N[backup_encrypt_prompt]="Зашифровать бекап паролем?"
I18N[backup_none]="Бекапов нет"
I18N[backup_list_title]="Доступные бекапы"
I18N[backup_pick_prompt]="Номер бекапа (или путь к файлу)"
I18N[backup_not_found]="Бекап не найден"
I18N[backup_file_not_found_fmt]="Файл не найден: %s"
I18N[backup_cleanup_fmt]="Удалено %s старых бекапов (оставлено %s)"