#!/bin/sh set -euo pipefail # ============================================================ # Telegram Shop — установщик # Скрипт для развёртывания на x86_64 и ARM64 (Orange Pi, RPi) # Совместим с POSIX sh (Alpine ash) # ============================================================ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' print_header() { printf "${CYAN}${BOLD}\n" printf "╔═══════════════════════════════════════════════════════════════╗\n" printf "║ Telegram Shop — Установщик v1.1 ║\n" printf "║ Поддержка x86_64 и ARM64 (Orange Pi, Raspberry Pi) ║\n" printf "╚═══════════════════════════════════════════════════════════════╝\n" printf "${NC}\n" } print_ok() { printf "${GREEN}✓${NC} %s\n" "$1"; } print_warn() { printf "${YELLOW}⚠${NC} %s\n" "$1"; } print_err() { printf "${RED}✗${NC} %s\n" "$1"; } print_info() { printf "${BLUE}ℹ${NC} %s\n" "$1"; } print_step() { printf "\n${CYAN}${BOLD}▶ %s${NC}\n" "$1"; } trap 'print_err "Установка прервана ошибкой (строка $LINENO)"; exit 1' ERR # ============================================================ # 0. Определение окружения # ============================================================ print_header print_step "Определение окружения" ARCH=$(uname -m) case "$ARCH" in x86_64) ARCH_NAME="x86_64 (Intel/AMD)" ;; aarch64|arm64) ARCH_NAME="ARM64 (Orange Pi / RPi)" ;; armv7l) ARCH_NAME="ARMv7 (Raspberry Pi 2)" ;; *) print_err "Неподдерживаемая архитектура: $ARCH" exit 1 ;; esac print_ok "Архитектура: $ARCH_NAME" print_info "Docker автоматически соберёт образ под текущую архитектуру" # ============================================================ # 1. Проверка / установка Docker # ============================================================ print_step "Проверка Docker" DOCKER_MISSING=0 if ! command -v docker >/dev/null 2>&1; then DOCKER_MISSING=1 elif ! docker version >/dev/null 2>&1; then DOCKER_MISSING=1 fi if [ "$DOCKER_MISSING" -eq 1 ]; then print_warn "Docker не установлен или не запущен" printf "\n" printf "Установить Docker сейчас? (y/N): " read -r INSTALL_DOCKER case "$INSTALL_DOCKER" in [Yy]|[Yy][Ee][Ss]) ;; *) print_err "Docker обязателен. Установите вручную: https://docs.docker.com/engine/install/" exit 1 ;; esac print_info "Установка Docker..." if command -v apk >/dev/null 2>&1; then # Alpine apk add --no-cache docker docker-cli-compose rc-service docker start 2>/dev/null || service docker start 2>/dev/null || true if ! docker version >/dev/null 2>&1; then print_err "Docker не запустился. Запустите вручную: rc-service docker start" exit 1 fi elif command -v apt-get >/dev/null 2>&1; then # Debian / Ubuntu / Armbian apt-get update apt-get install -y ca-certificates curl gnupg install -m 0755 -d /etc/apt/keyrings curl -fsSL "https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg" | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg printf "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$(. /etc/os-release && echo "$ID") $(. /etc/os-release && echo "$VERSION_CODENAME") stable\n" \ > /etc/apt/sources.list.d/docker.list apt-get update apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin service docker start 2>/dev/null || systemctl start docker 2>/dev/null || true if ! docker version >/dev/null 2>&1; then print_err "Docker не запустился. Запустите вручную: systemctl start docker" exit 1 fi else print_err "Неизвестный пакетный менеджер. Установите Docker вручную." exit 1 fi print_ok "Docker установлен" else print_ok "Docker установлен: $(docker --version)" fi # ============================================================ # 2. Проверка docker compose # ============================================================ print_step "Проверка docker compose" COMPOSE_CMD="" if docker compose version >/dev/null 2>&1; then COMPOSE_CMD="docker compose" COMPOSE_VER=$(docker compose version --short 2>/dev/null || echo "v2") print_ok "docker compose v2 доступен: $COMPOSE_VER" elif command -v docker-compose >/dev/null 2>&1; then COMPOSE_CMD="docker-compose" print_ok "docker-compose v1 доступен: $(docker-compose --version)" else print_err "docker compose не найден. Установите docker-compose-plugin или docker-compose." exit 1 fi # ============================================================ # 3. Подготовка файлов окружения # ============================================================ print_step "Подготовка .env" if [ ! -f ".env" ]; then if [ -f ".env.example" ]; then cp .env.example .env # Убираем CRLF если файл скопирован из Windows if command -v sed >/dev/null 2>&1; then sed -i 's/\r$//' .env fi print_ok ".env создан из .env.example" else print_err ".env.example не найден!" exit 1 fi else print_ok ".env уже существует — пропускаю создание" # Убираем CRLF в существующем файле тоже if command -v sed >/dev/null 2>&1; then sed -i 's/\r$//' .env fi fi # ============================================================ # 4. Настройка переменных окружения (интерактивно) # ============================================================ print_step "Настройка переменных окружения (интерактивно)" # Функция для обновления переменной в .env (безопасно для спецсимволов в значении) update_env_var() { var_name="$1" var_value="$2" if grep -q "^${var_name}=" .env 2>/dev/null; then awk -v key="$var_name" -v val="$var_value" ' BEGIN { FS = OFS = "="; done = 0 } /^[A-Za-z_][A-Za-z0-9_]*=/ { if ($1 == key) { print key "=" val; done = 1; next } } { print } END { if (!done) print key "=" val } ' .env > .env.tmp && mv .env.tmp .env else echo "${var_name}=${var_value}" >> .env fi } # Скрытый ввод секретов (POSIX sh, работает в Alpine ash) read_secret() { printf "%s" "$1" stty -echo 2>/dev/null read -r input stty echo 2>/dev/null printf "\n" } # Функция для генерации случайного hex-строки (32 байта) generate_random_hex() { if command -v node >/dev/null 2>&1; then node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" elif command -v openssl >/dev/null 2>&1; then openssl rand -hex 32 else od -An -tx32 /dev/urandom | tr -d ' \n' fi } # BOT_TOKEN current_bot_token=$(grep -E "^BOT_TOKEN=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true if [ -z "$current_bot_token" ] || [ "$current_bot_token" = "your_bot_token_here" ]; then print_warn "BOT_TOKEN не задан или содержит placeholder." read_secret "Введите BOT_TOKEN (от @BotFather): " update_env_var "BOT_TOKEN" "$input" bot_token="$input" else bot_token="$current_bot_token" fi # ADMIN_IDS current_admin_ids=$(grep -E "^ADMIN_IDS=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true if [ -z "$current_admin_ids" ] || [ "$current_admin_ids" = "123456789,987654321" ]; then print_warn "ADMIN_IDS не задан или содержит placeholder." printf "Введите ADMIN_IDS (через запятую, например: 123456789,987654321): " read -r input update_env_var "ADMIN_IDS" "$input" admin_ids="$input" else admin_ids="$current_admin_ids" fi # ENCRYPTION_KEY current_encryption_key=$(grep -E "^ENCRYPTION_KEY=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true if [ -z "$current_encryption_key" ]; then encryption_key=$(generate_random_hex) update_env_var "ENCRYPTION_KEY" "$encryption_key" print_ok "ENCRYPTION_KEY сгенерирован автоматически" else encryption_key="$current_encryption_key" fi # ADMIN_SECRET current_admin_secret=$(grep -E "^ADMIN_SECRET=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true if [ -z "$current_admin_secret" ] || [ "$current_admin_secret" = "changeme" ]; then admin_secret=$(generate_random_hex) update_env_var "ADMIN_SECRET" "$admin_secret" print_ok "ADMIN_SECRET сгенерирован автоматически" else admin_secret="$current_admin_secret" fi # SUPER_ADMIN_SECRET current_super_admin_secret=$(grep -E "^SUPER_ADMIN_SECRET=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true if [ -z "$current_super_admin_secret" ] || [ "$current_super_admin_secret" = "changeme_super" ]; then # Генерация до тех пор, пока не получим значение, отличное от ADMIN_SECRET super_admin_secret="" while [ -z "$super_admin_secret" ] || [ "$super_admin_secret" = "$admin_secret" ]; do super_admin_secret=$(generate_random_hex) done update_env_var "SUPER_ADMIN_SECRET" "$super_admin_secret" print_ok "SUPER_ADMIN_SECRET сгенерирован автоматически" else super_admin_secret="$current_super_admin_secret" fi # SHOP_ACTIVATED current_shop_activated=$(grep -E "^SHOP_ACTIVATED=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true if [ -z "$current_shop_activated" ]; then shop_activated="false" update_env_var "SHOP_ACTIVATED" "$shop_activated" print_info "SHOP_ACTIVATED установлен в 'false' (магазин стартует заблокированным)" else shop_activated="$current_shop_activated" fi # ADMIN_URL current_admin_url=$(grep -E "^ADMIN_URL=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true if [ -z "$current_admin_url" ]; then # Попытка определить LAN IP lan_ip=$(hostname -I | awk '{print $1}') if [ -z "$lan_ip" ]; then lan_ip=$(ip route get 1 | awk '{print $7}' 2>/dev/null) fi if [ -n "$lan_ip" ]; then admin_url="http://${lan_ip}:3000" update_env_var "ADMIN_URL" "$admin_url" print_info "ADMIN_URL установлен в $admin_url (определен автоматически)" else print_warn "Не удалось определить LAN IP. ADMIN_URL не установлен." fi else admin_url="$current_admin_url" fi # Ограничиваем доступ к .env (содержит секреты) if [ -f .env ]; then chmod 600 .env print_ok ".env защищён (chmod 600)" fi # ============================================================ # 5. Создание директорий # ============================================================ print_step "Создание директорий" mkdir -p db uploads print_ok "db/, uploads/ готовы" # ============================================================ # 6. Загрузка образов из Gitea Container Registry # ============================================================ print_step "Загрузка образов из Gitea Container Registry" print_info "Архитектура: $ARCH_NAME" print_info "Образы собираются в CI (Gitea Actions, buildx multi-arch) — на сервере ничего не компилируется" # Релизная версия: по умолчанию latest, можно переопределить IMAGE_TAG=v1.2.9 IMAGE_TAG="${IMAGE_TAG:-latest}" print_info "Используемая версия образа: $IMAGE_TAG" $COMPOSE_CMD pull telegram_shop_prod tg_shop_admin print_ok "Образы telegram-shop:${IMAGE_TAG} и telegram-shop-admin:${IMAGE_TAG} загружены" # ============================================================ # 7. Запуск контейнеров # ============================================================ print_step "Запуск контейнеров" IMAGE_TAG="${IMAGE_TAG}" $COMPOSE_CMD up -d print_ok "Контейнеры запущены" # ============================================================ # 8. Проверка статуса # ============================================================ print_step "Проверка статуса" sleep 3 $COMPOSE_CMD ps # ============================================================ # 9. Показ логов # ============================================================ print_step "Последние логи" $COMPOSE_CMD logs --tail=20 # ============================================================ # 10. Health-check с повторными попытками # ============================================================ print_step "Проверка работоспособности" HEALTH_URL="http://localhost:3001/health" print_info "Запрос: $HEALTH_URL" HEALTH_OK=0 for ATTEMPT in 1 2 3 4 5 6; do sleep 5 if curl -sf "$HEALTH_URL" >/dev/null 2>&1; then HEALTH_OK=1 break fi print_info "Попытка $ATTEMPT/6 — сервис ещё запускается..." done if [ "$HEALTH_OK" -eq 1 ]; then RESPONSE=$(curl -sf "$HEALTH_URL" 2>/dev/null || echo "ok") print_ok "Сервис отвечает!" printf " ${GREEN}Ответ: %s${NC}\n" "$RESPONSE" else print_warn "Health-check не ответил за 30 секунд. Проверьте логи:" printf " curl %s\n" "$HEALTH_URL" printf " %s logs -f\n" "$COMPOSE_CMD" fi # ============================================================ # 11. Tor Proxy — onion-адреса # ============================================================ printf "\n" print_step "Проверка Tor-прокси" TOR_RUNNING=0 for ATTEMPT in 1 2 3 4 5 6; do sleep 5 if docker exec tor-proxy test -s /var/lib/tor/ssh/hostname 2>/dev/null && \ docker exec tor-proxy test -s /var/lib/tor/admin/hostname 2>/dev/null; then TOR_RUNNING=1 break fi print_info "Попытка $ATTEMPT/6 — Tor генерирует onion-адреса..." done if [ "$TOR_RUNNING" -eq 1 ]; then SSH_ONION=$(docker exec tor-proxy cat /var/lib/tor/ssh/hostname 2>/dev/null || echo "") ADMIN_ONION=$(docker exec tor-proxy cat /var/lib/tor/admin/hostname 2>/dev/null || echo "") if [ -n "$SSH_ONION" ] && [ -n "$ADMIN_ONION" ]; then print_ok "Tor-прокси работает!" printf "\n ${CYAN}${BOLD}SSH onion:${NC} %s\n" "$SSH_ONION" printf " ${CYAN}${BOLD}Admin onion:${NC} %s\n" "$ADMIN_ONION" printf "\n ${BOLD}Подключение:${NC}\n" printf " SSH: torify ssh root@%s\n" "$SSH_ONION" printf " Admin: откройте http://%s в Tor Browser\n" "$ADMIN_ONION" if [ -f "./tor-proxy/get-onions.sh" ]; then sh ./tor-proxy/get-onions.sh 2>/dev/null || true fi else print_warn "Tor запущен, но onion-адреса не найдены" fi else print_warn "Tor-прокси не стартовал за 30 секунд. Проверьте логи:" printf " docker logs tor-proxy\n" fi # ============================================================ # 12. Финальная сводка # ============================================================ printf "\n" print_step "Финальная сводка" # LAN IP для админки (если не определен ранее) if [ -z "${admin_url:-}" ]; then lan_ip=$(hostname -I | awk '{print $1}') if [ -z "$lan_ip" ]; then lan_ip=$(ip route get 1 | awk '{print $7}' 2>/dev/null) fi if [ -n "$lan_ip" ]; then admin_url="http://${lan_ip}:3000" else admin_url="Не определен" fi fi printf " ${CYAN}${BOLD}LAN URL админки:${NC} %s\n" "$admin_url" printf " ${CYAN}${BOLD}SSH onion:${NC} %s\n" "$SSH_ONION" printf " ${CYAN}${BOLD}Admin onion:${NC} %s\n" "$ADMIN_ONION" printf " ${CYAN}${BOLD}ADMIN_SECRET:${NC} %s\n" "$admin_secret" printf " ${CYAN}${BOLD}SUPER_ADMIN_SECRET:${NC} %s\n" "$super_admin_secret" printf "\n" printf "${YELLOW}⚠ Магазин стартует заблокированным (SHOP_ACTIVATED=false).${NC}\n" printf "${YELLOW} Войдите как супер-админ, добавьте комиссионные кошельки и активируйте магазин в Настройках.${NC}\n" printf "\n" # ============================================================ # Готово # ============================================================ printf "\n" printf "${GREEN}${BOLD}╔═══════════════════════════════════════════════════════════════╗${NC}\n" printf "${GREEN}${BOLD}║ Установка завершена! ║${NC}\n" printf "${GREEN}${BOLD}╚═══════════════════════════════════════════════════════════════╝${NC}\n" printf "\n" printf "${BOLD}Полезные команды:${NC}\n" printf " docker compose ps # статус контейнеров\n" printf " docker compose logs -f # логи в реальном времени\n" printf " docker compose restart # перезапустить\n" printf " docker compose down # остановить\n" printf " docker compose up -d --build # пересобрать и запустить\n" printf "\n" printf "${BOLD}Health-check:${NC} %s\n" "$HEALTH_URL"