#!/bin/bash # MY Network v3.0 - Автоматическая установка и запуск ноды # Версия: 3.0.0 set -eE # Цвета для вывода RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' WHITE='\033[1;37m' NC='\033[0m' # No Color # Переменные по умолчанию SCRIPT_VERSION="3.0.0" PROJECT_DIR="/opt/my-network" STORAGE_DIR="/opt/my-network/storage" CONFIG_DIR="/opt/my-network/config" LOGS_DIR="/opt/my-network/logs" # Параметры ноды NODE_TYPE="" NETWORK_MODE="" ALLOW_INCOMING="false" DOCKER_SOCK_PATH="/var/run/docker.sock" BOOTSTRAP_CONFIG="" TELEGRAM_API_KEY="" CLIENT_TELEGRAM_API_KEY="" NODE_VERSION="3.0.0" ENABLE_SSL="${ENABLE_SSL:-false}" DOMAIN="${DOMAIN:-}" EMAIL="${EMAIL:-}" ENABLE_WEB_CLIENT="${ENABLE_WEB_CLIENT:-true}" # Функция логирования log() { echo -e "${WHITE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1" } log_info() { echo -e "${BLUE}[INFO]${NC} $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } log_warn() { echo -e "${YELLOW}[WARNING]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } # Утилиты и Docker Compose обнаружение has_cmd() { command -v "$1" >/dev/null 2>&1; } COMPOSE_CMD="" init_compose_cmd() { if has_cmd docker && docker compose version >/dev/null 2>&1; then COMPOSE_CMD="docker compose" elif has_cmd docker-compose; then COMPOSE_CMD="docker-compose" else COMPOSE_CMD="" fi } # Показать заставку show_banner() { clear echo -e "${PURPLE}" cat << "EOF" ╔══════════════════════════════════════════════════════════════╗ ║ MY Network v3.0 ║ ║ Децентрализованная сеть контента ║ ║ ║ ║ • Мгновенная трансляция без расшифровки ║ ║ • Блокчейн интеграция для uploader-bot ║ ║ • Полная децентрализация без консенсуса ║ ║ • Автоматическая конвертация через Docker ║ ╚══════════════════════════════════════════════════════════════╝ EOF echo -e "${NC}" echo "" log_info "Запуск автоматической установки MY Network v3.0..." echo "" } # Проверка прав root check_root() { if [[ $EUID -ne 0 ]]; then log_error "Этот скрипт должен запускаться с правами root (sudo)" echo "Использование: sudo bash start.sh" exit 1 fi } # Определение операционной системы detect_os() { if [ -f /etc/os-release ]; then . /etc/os-release OS=$NAME VER=$VERSION_ID OS_ID=$ID else log_error "Не удалось определить операционную систему" exit 1 fi log_info "Обнаружена ОС: $OS $VER" # Проверка поддерживаемых ОС case $OS_ID in ubuntu|debian|centos|rhel|fedora) log_success "Операционная система поддерживается" ;; *) log_warn "Операционная система может быть не полностью поддержана" if check_interactive; then echo -n "Продолжить установку? [y/N]: " >&2 read -r REPLY < /dev/tty if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi else log_info "Неинтерактивный режим: продолжаем с неподдерживаемой ОС" fi ;; esac } # Проверка существующей установки check_existing_installation() { log_info "🔍 Проверка существующей установки MY Network..." local existing_installation=false local services_running=false # Проверяем наличие папки проекта if [ -d "$PROJECT_DIR" ]; then log_info "Обнаружена папка проекта: $PROJECT_DIR" existing_installation=true fi # Проверяем systemd сервис if has_cmd systemctl && systemctl list-unit-files | grep -q "my-network.service"; then log_info "Обнаружен systemd сервис: my-network" existing_installation=true if systemctl is-active my-network >/dev/null 2>&1; then log_info "Сервис my-network активен" services_running=true fi fi # Проверяем Docker контейнеры if has_cmd docker; then if docker ps -a --format "table {{.Names}}" | grep -q "my-network"; then log_info "Обнаружены Docker контейнеры MY Network" existing_installation=true if docker ps --format "table {{.Names}}" | grep -q "my-network"; then log_info "Найдены запущенные контейнеры MY Network" services_running=true fi fi fi # Проверяем Docker образы if has_cmd docker; then if docker images --format "table {{.Repository}}" | grep -q "my-network"; then log_info "Обнаружены Docker образы MY Network" existing_installation=true fi fi if [ "$existing_installation" = true ]; then echo "" echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE} ОБНАРУЖЕНА СУЩЕСТВУЮЩАЯ УСТАНОВКА ${NC}" echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" if [ "$services_running" = true ]; then log_warn "Обнаружены запущенные сервисы MY Network" fi echo -e "${WHITE}Найдены компоненты предыдущей установки MY Network.${NC}" echo -e "${WHITE}Для корректного обновления необходимо выполнить очистку.${NC}" echo "" if check_interactive; then echo -n "Обновить существующую установку MY Network? [y/N]: " >&2 read -r update_choice < /dev/tty if [[ ! $update_choice =~ ^[Yy]$ ]]; then log_info "Обновление отменено пользователем" echo "" echo -e "${CYAN}Для ручного управления используйте:${NC}" echo -e "${BLUE}systemctl stop my-network${NC} # Остановка сервиса" echo -e "${BLUE}docker compose -f $PROJECT_DIR/my-network/docker-compose.yml down${NC} # Остановка контейнеров" echo -e "${BLUE}sudo rm -rf $PROJECT_DIR${NC} # Удаление проекта" echo "" exit 0 fi else log_warn "Неинтерактивный режим: существующая установка будет автоматически обновлена" log_info "Для предотвращения обновления запустите скрипт локально" sleep 3 fi cleanup_existing_installation else log_success "Предыдущие установки не обнаружены. Выполняется чистая установка." fi } # Очистка существующей установки cleanup_existing_installation() { log_info "🧹 Очистка существующей установки..." # 1. Остановка systemd сервиса if systemctl is-active my-network >/dev/null 2>&1; then log_info "Остановка systemd сервиса my-network..." systemctl stop my-network || log_warn "Не удалось остановить сервис" systemctl disable my-network >/dev/null 2>&1 || true fi # 2. Остановка и удаление Docker контейнеров log_info "Остановка и удаление Docker контейнеров..." # Переходим в папку проекта если существует if [ -d "$PROJECT_DIR/my-network" ]; then cd "$PROJECT_DIR/my-network" init_compose_cmd if [ -n "$COMPOSE_CMD" ]; then $COMPOSE_CMD down --remove-orphans --volumes 2>/dev/null || true fi fi # Принудительная остановка всех контейнеров MY Network local containers=$(docker ps -a --filter "name=my-network" --format "{{.ID}}" 2>/dev/null || true) if [ -n "$containers" ]; then log_info "Удаление контейнеров MY Network..." echo "$containers" | xargs docker rm -f 2>/dev/null || true fi # 3. Удаление Docker образов log_info "Удаление Docker образов MY Network..." local images=$(docker images --filter "reference=my-network*" --format "{{.ID}}" 2>/dev/null || true) if [ -n "$images" ]; then echo "$images" | xargs docker rmi -f 2>/dev/null || true fi # Удаление converter образа docker rmi my-network-converter:latest 2>/dev/null || true # 4. Очистка Docker системы log_info "Очистка Docker кэша и неиспользуемых ресурсов..." docker system prune -f 2>/dev/null || true docker builder prune -f 2>/dev/null || true # 5. Удаление systemd сервиса if [ -f "/etc/systemd/system/my-network.service" ]; then log_info "Удаление systemd сервиса..." rm -f /etc/systemd/system/my-network.service systemctl daemon-reload fi # 6. Остановка nginx если настроен для MY Network if systemctl is-active nginx >/dev/null 2>&1; then if [ -f "/etc/nginx/sites-enabled/my-network" ]; then log_info "Удаление nginx конфигурации MY Network..." rm -f /etc/nginx/sites-enabled/my-network rm -f /etc/nginx/sites-available/my-network # Восстанавливаем дефолтную конфигурацию nginx если есть backup if [ -f /etc/nginx/nginx.conf.backup ]; then cp /etc/nginx/nginx.conf.backup /etc/nginx/nginx.conf fi # Перезапускаем nginx systemctl reload nginx 2>/dev/null || systemctl restart nginx 2>/dev/null || true fi fi # 7. Удаление веб-файлов if [ -d "/var/www/my-network-web" ]; then log_info "Удаление веб-файлов..." rm -rf /var/www/my-network-web fi # 8. Вопрос о базе данных local remove_database=false echo "" echo -e "${PURPLE}❓ База данных:${NC}" if check_interactive; then echo -n "Удалить существующую базу данных? [y/N]: " >&2 read -r db_choice < /dev/tty if [[ $db_choice =~ ^[Yy]$ ]]; then remove_database=true log_warn "База данных будет удалена" else log_info "База данных будет сохранена для миграции" fi else log_info "Неинтерактивный режим: база данных сохраняется для миграции" fi # 9. Удаление файлов проекта (кроме БД если сохраняем) if [ -d "$PROJECT_DIR" ]; then log_info "Удаление файлов проекта..." if [ "$remove_database" = true ]; then # Удаляем все включая БД rm -rf "$PROJECT_DIR" rm -rf "$STORAGE_DIR" 2>/dev/null || true rm -rf "$CONFIG_DIR" 2>/dev/null || true rm -rf "$LOGS_DIR" 2>/dev/null || true log_info "Проект полностью удален включая базу данных" else # Сохраняем только docker volumes с БД backup_dir="/tmp/my-network-db-backup-$(date +%s)" # Создаем резервную копию volumes перед удалением if has_cmd docker && docker volume ls | grep -q "my-network.*postgres"; then log_info "Создание резервной копии базы данных..." mkdir -p "$backup_dir" # Экспортируем данные PostgreSQL if docker run --rm -v my-network_postgres_data:/source -v "$backup_dir":/backup alpine tar czf /backup/postgres_data.tar.gz -C /source . 2>/dev/null; then log_success "Резервная копия создана: $backup_dir/postgres_data.tar.gz" else log_warn "Не удалось создать резервную копию БД" fi fi # Удаляем все файлы проекта rm -rf "$PROJECT_DIR" rm -rf "$STORAGE_DIR" 2>/dev/null || true rm -rf "$LOGS_DIR" 2>/dev/null || true # Сохраняем только папку config для ключей если есть if [ -d "$CONFIG_DIR" ]; then config_backup="$CONFIG_DIR.backup-$(date +%s)" mv "$CONFIG_DIR" "$config_backup" 2>/dev/null || true log_info "Конфигурация сохранена: $config_backup" fi log_info "Проект удален, база данных сохранена" fi fi # 10. Очистка Docker volumes (только если удаляем БД) if [ "$remove_database" = true ]; then log_info "Удаление Docker volumes..." if has_cmd docker; then docker volume ls | grep "my-network" | awk '{print $2}' | xargs -r docker volume rm 2>/dev/null || true fi fi log_success "Очистка завершена" echo "" } # Проверка доступности TTY для интерактивного ввода check_interactive() { if [ -t 0 ] && [ -t 1 ]; then return 0 # TTY доступен else return 1 # TTY недоступен fi } # Безопасное чтение ввода safe_read() { local prompt="$1" local default="$2" local var_name="$3" if check_interactive; then # Интерактивный режим - используем /dev/tty echo -n "$prompt" >&2 read -r input < /dev/tty if [ -n "$input" ]; then eval "$var_name='$input'" else eval "$var_name='$default'" fi else # Неинтерактивный режим - используем значения по умолчанию log_info "Неинтерактивный режим: $prompt -> используется значение по умолчанию: $default" eval "$var_name='$default'" fi } # Интерактивная настройка interactive_setup() { echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE} НАСТРОЙКА MY NETWORK v3.0 ${NC}" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" # Проверяем режим if ! check_interactive; then log_warn "Обнаружен неинтерактивный режим (curl | bash)" log_info "Используются настройки по умолчанию. Для интерактивной настройки скачайте и запустите скрипт локально:" log_info "wget https://git.projscale.dev/my-dev/uploader-bot/raw/branch/main/start.sh && chmod +x start.sh && sudo ./start.sh" echo "" fi # 1. Тип сети echo -e "${PURPLE}❓ Выберите режим работы сети:${NC}" echo " 1) Создать новую сеть (Bootstrap нода)" echo " 2) Подключиться к существующей сети" echo "" if check_interactive; then while true; do echo -n "Введите номер [1-2]: " >&2 read -r network_choice < /dev/tty case $network_choice in 1) NETWORK_MODE="bootstrap" NODE_TYPE="bootstrap" log_info "Режим: Создание новой сети (Bootstrap нода)" break ;; 2) NETWORK_MODE="existing" log_info "Режим: Подключение к существующей сети" break ;; *) log_error "Неверный выбор. Введите 1 или 2." ;; esac done else # Неинтерактивный режим - создаем новую сеть по умолчанию NETWORK_MODE="bootstrap" NODE_TYPE="bootstrap" log_info "Неинтерактивный режим: используется режим создания новой сети (Bootstrap нода)" fi # 2. Тип ноды (если подключаемся к существующей) if [ "$NETWORK_MODE" = "existing" ]; then echo "" echo -e "${PURPLE}❓ Выберите тип ноды:${NC}" echo " 1) Публичная нода (принимает входящие соединения)" echo " 2) Приватная нода (только исходящие соединения)" echo "" if check_interactive; then while true; do echo -n "Введите номер [1-2]: " >&2 read -r node_choice < /dev/tty case $node_choice in 1) NODE_TYPE="public" ALLOW_INCOMING="true" log_info "Тип ноды: Публичная (открытые порты)" break ;; 2) NODE_TYPE="private" ALLOW_INCOMING="false" log_info "Тип ноды: Приватная (закрытые порты)" break ;; *) log_error "Неверный выбор. Введите 1 или 2." ;; esac done else # Неинтерактивный режим - публичная нода по умолчанию NODE_TYPE="public" ALLOW_INCOMING="true" log_info "Неинтерактивный режим: используется публичная нода по умолчанию" fi else ALLOW_INCOMING="true" # Bootstrap нода всегда принимает подключения fi # 3. Bootstrap конфигурация echo "" echo -e "${PURPLE}❓ Конфигурация bootstrap узлов:${NC}" if [ "$NETWORK_MODE" = "bootstrap" ]; then log_info "Bootstrap нода будет создавать новую сеть" BOOTSTRAP_CONFIG="new" else if check_interactive; then echo -n "Путь до bootstrap.json [Enter для дефолтного]: " >&2 read -r custom_bootstrap < /dev/tty else custom_bootstrap="" log_info "Неинтерактивный режим: используется дефолтный bootstrap.json" fi if [ -n "$custom_bootstrap" ] && [ -f "$custom_bootstrap" ]; then BOOTSTRAP_CONFIG="$custom_bootstrap" log_success "Использован кастомный bootstrap.json: $custom_bootstrap" else BOOTSTRAP_CONFIG="default" log_info "Используется дефолтный bootstrap.json" fi fi # 4. Docker socket echo "" echo -e "${PURPLE}❓ Настройка Docker для конвертации:${NC}" if check_interactive; then echo -n "Путь до docker.sock [$DOCKER_SOCK_PATH]: " >&2 read -r custom_docker_sock < /dev/tty else custom_docker_sock="" log_info "Неинтерактивный режим: используется дефолтный путь $DOCKER_SOCK_PATH" fi if [ -n "$custom_docker_sock" ]; then DOCKER_SOCK_PATH="$custom_docker_sock" fi if [ -S "$DOCKER_SOCK_PATH" ]; then log_success "Docker socket найден: $DOCKER_SOCK_PATH" else log_warn "Docker socket не найден: $DOCKER_SOCK_PATH" log_info "Docker будет установлен автоматически" fi # 5. Настройка веб-клиента echo "" echo -e "${PURPLE}❓ Настройка веб-интерфейса:${NC}" if check_interactive && [ -z "${ENABLE_WEB_CLIENT}" ]; then echo -n "Развернуть веб-клиент для управления нодой? [Y/n]: " >&2 read -r web_choice < /dev/tty else web_choice="Y" log_info "Неинтерактивный режим: веб-клиент включен по умолчанию" fi if [[ ! $web_choice =~ ^[Nn]$ ]]; then ENABLE_WEB_CLIENT="true" log_success "Веб-клиент будет развернут" else ENABLE_WEB_CLIENT="false" log_info "Веб-клиент отключен" fi # 6. SSL и домен (только для публичных нод с веб-клиентом) if [ "$ALLOW_INCOMING" = "true" ] && [ "$ENABLE_WEB_CLIENT" = "true" ]; then echo "" echo -e "${PURPLE}❓ Настройка SSL сертификата:${NC}" if [ -n "$DOMAIN" ] && [ -n "$EMAIL" ]; then ssl_choice="Y" log_info "Обнаружены DOMAIN и EMAIL в окружении — включаем SSL автоматически" else if check_interactive; then echo -n "Настроить SSL сертификат? [y/N]: " >&2 read -r ssl_choice < /dev/tty else ssl_choice="N" log_info "Неинтерактивный режим: SSL отключен (требует ручной настройки)" fi fi if [[ $ssl_choice =~ ^[Yy]$ ]]; then if [ -z "$DOMAIN" ]; then echo -n "Доменное имя: " >&2 read -r DOMAIN < /dev/tty fi if [ -n "$DOMAIN" ] && [ -z "$EMAIL" ]; then echo -n "Email для уведомлений SSL: " >&2 read -r EMAIL < /dev/tty fi if [ -n "$DOMAIN" ] && [ -n "$EMAIL" ]; then ENABLE_SSL="true" log_success "SSL будет настроен для домена: $DOMAIN" else log_error "DOMAIN/EMAIL не указаны. SSL отключен" fi fi else log_info "SSL недоступен для данной конфигурации" fi # 7. Telegram API ключи echo "" echo -e "${PURPLE}❓ Настройка Telegram ботов (необязательно):${NC}" if check_interactive; then echo -n "TELEGRAM_API_KEY (основной бот) [Enter для пропуска]: " >&2 read -r TELEGRAM_API_KEY < /dev/tty else TELEGRAM_API_KEY="" log_info "Неинтерактивный режим: Telegram боты отключены" fi if [ -n "$TELEGRAM_API_KEY" ]; then log_success "TELEGRAM_API_KEY настроен" if check_interactive; then echo -n "CLIENT_TELEGRAM_API_KEY (клиентский бот) [Enter для пропуска]: " >&2 read -r CLIENT_TELEGRAM_API_KEY < /dev/tty else CLIENT_TELEGRAM_API_KEY="" fi if [ -n "$CLIENT_TELEGRAM_API_KEY" ]; then log_success "CLIENT_TELEGRAM_API_KEY настроен" else log_info "Клиентский Telegram бот будет отключен" fi else log_info "Telegram боты будут отключены" fi # 6. Подтверждение настроек echo "" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE} ПОДТВЕРЖДЕНИЕ НАСТРОЕК ${NC}" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo -e "${WHITE}Режим сети:${NC} $NETWORK_MODE" echo -e "${WHITE}Тип ноды:${NC} $NODE_TYPE" echo -e "${WHITE}Входящие соединения:${NC} $ALLOW_INCOMING" echo -e "${WHITE}Docker socket:${NC} $DOCKER_SOCK_PATH" echo -e "${WHITE}Bootstrap config:${NC} $BOOTSTRAP_CONFIG" echo -e "${WHITE}Веб-клиент:${NC} $([ "$ENABLE_WEB_CLIENT" = "true" ] && echo "включен" || echo "отключен")" echo -e "${WHITE}SSL сертификат:${NC} $([ "$ENABLE_SSL" = "true" ] && echo "включен для $DOMAIN" || echo "отключен")" echo -e "${WHITE}Telegram основной:${NC} $([ -n "$TELEGRAM_API_KEY" ] && echo "настроен" || echo "отключен")" echo -e "${WHITE}Telegram клиентский:${NC} $([ -n "$CLIENT_TELEGRAM_API_KEY" ] && echo "настроен" || echo "отключен")" echo "" if check_interactive; then echo -n "Продолжить установку с этими настройками? [Y/n]: " >&2 read -r REPLY < /dev/tty if [[ $REPLY =~ ^[Nn]$ ]]; then log_info "Установка отменена пользователем" exit 0 fi else log_info "Неинтерактивный режим: продолжаем установку с настройками по умолчанию" sleep 3 fi } # Установка зависимостей install_dependencies() { log_info "📦 Установка системных зависимостей..." # Настройка неинтерактивного режима для apt export DEBIAN_FRONTEND=noninteractive export NEEDRESTART_MODE=a export NEEDRESTART_SUSPEND=1 # Обновление пакетов case $OS_ID in ubuntu|debian) # Настройка для автоматического согласия с дефолтными настройками echo 'DPkg::Options {"--force-confdef";"--force-confold";}' > /etc/apt/apt.conf.d/50unattended-upgrades-local apt-get update -qq apt-get upgrade -y -qq -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" apt-get install -y -qq curl wget git unzip htop nano ufw fail2ban \ python3 python3-pip python3-venv build-essential \ postgresql-client jq netcat-openbsd ;; centos|rhel|fedora) if command -v dnf &> /dev/null; then dnf update -y -q dnf install -y -q curl wget git unzip htop nano firewalld \ python3 python3-pip python3-devel gcc gcc-c++ \ postgresql jq nc else yum update -y -q yum install -y -q curl wget git unzip htop nano firewalld \ python3 python3-pip python3-devel gcc gcc-c++ \ postgresql jq nc fi ;; esac # Очистка переменных окружения unset DEBIAN_FRONTEND unset NEEDRESTART_MODE unset NEEDRESTART_SUSPEND log_success "Системные зависимости установлены" } # Установка Docker и Docker Compose install_docker() { if command -v docker &> /dev/null; then log_info "Docker уже установлен: $(docker --version)" else log_info "🐳 Установка Docker..." # Установка Docker через официальный скрипт curl -fsSL https://get.docker.com -o get-docker.sh sh get-docker.sh rm get-docker.sh # Запуск и автозагрузка Docker systemctl start docker systemctl enable docker log_success "Docker установлен: $(docker --version)" fi # Проверка Docker Compose (предпочитаем docker compose plugin) init_compose_cmd if [ -n "$COMPOSE_CMD" ]; then log_info "Compose доступен: $($COMPOSE_CMD version 2>/dev/null | head -n1)" else log_info "🐳 Установка Docker Compose..." case $OS_ID in ubuntu|debian) # Попытаться установить plugin через apt apt-get update -qq || true apt-get install -y -qq docker-compose-plugin || true ;; centos|rhel|fedora) if command -v dnf &>/dev/null; then dnf install -y -q docker-compose-plugin || true else yum install -y -q docker-compose-plugin || true fi ;; esac init_compose_cmd if [ -z "$COMPOSE_CMD" ]; then # Фоллбэк: скачать статический бинарник docker-compose v2 COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | jq -r .tag_name 2>/dev/null || echo "v2.24.7") curl -fsSL "https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose if /usr/local/bin/docker-compose version >/dev/null 2>&1; then COMPOSE_CMD="/usr/local/bin/docker-compose" fi fi if [ -n "$COMPOSE_CMD" ]; then log_success "Docker Compose установлен: $($COMPOSE_CMD version 2>/dev/null | head -n1)" else log_error "Не удалось установить Docker Compose" exit 1 fi fi # Проверка доступности Docker socket if [ ! -S "$DOCKER_SOCK_PATH" ]; then log_error "Docker socket недоступен: $DOCKER_SOCK_PATH" log_info "Перезапуск Docker..." systemctl restart docker sleep 5 if [ ! -S "$DOCKER_SOCK_PATH" ]; then log_error "Не удалось настроить Docker socket" exit 1 fi fi log_success "Docker socket доступен: $DOCKER_SOCK_PATH" } # Создание директорий проекта create_directories() { log_info "📁 Создание структуры директорий..." # Основные директории mkdir -p "$PROJECT_DIR" mkdir -p "$STORAGE_DIR" mkdir -p "$CONFIG_DIR" mkdir -p "$LOGS_DIR" # Поддиректории для хранения mkdir -p "$STORAGE_DIR" # Права доступа chmod 755 "$PROJECT_DIR" chmod 755 "$STORAGE_DIR" chmod 700 "$CONFIG_DIR" chmod 755 "$LOGS_DIR" log_success "Структура директорий создана" log_info "Проект: $PROJECT_DIR" log_info "Хранилище: $STORAGE_DIR" log_info "Конфигурация: $CONFIG_DIR" log_info "Логи: $LOGS_DIR" } # Клонирование репозиториев clone_repositories() { log_info "📥 Клонирование репозиториев MY Network v3.0..." cd "$PROJECT_DIR" # Если проект уже существует, удаляем старую версию if [ -d "my-network" ]; then log_info "Удаление существующего проекта..." rm -rf my-network fi # Создаем структуру проекта mkdir -p my-network cd my-network # Клонирование всех репозиториев log_info "Клонирование uploader-bot..." if git clone https://git.projscale.dev/my-dev/uploader-bot.git .; then log_success "uploader-bot клонирован" else log_error "Ошибка клонирования uploader-bot" exit 1 fi log_info "Клонирование web2-client..." if git clone https://git.projscale.dev/my-dev/web2-client.git web2-client; then log_success "web2-client клонирован" else log_error "Ошибка клонирования web2-client" exit 1 fi log_info "Клонирование converter-module..." if git clone https://git.projscale.dev/my-dev/converter-module.git converter-module; then log_success "converter-module клонирован" else log_error "Ошибка клонирования converter-module" exit 1 fi log_success "Все репозитории клонированы в $PROJECT_DIR/my-network" } # Создание файлов проекта create_project_files() { log_info "📝 Создание файлов проекта..." cd "$PROJECT_DIR/my-network" # Создание docker-compose.yml cat > docker-compose.yml << 'EOF' services: app: build: . container_name: my-network-app restart: unless-stopped ports: - "8000:8000" volumes: - ${STORAGE_PATH:-./storage}:/app/storage - ${DOCKER_SOCK_PATH:-/var/run/docker.sock}:/var/run/docker.sock - ./logs:/app/logs - ./config/keys:/app/keys:ro environment: - DATABASE_URL=${DATABASE_URL} - REDIS_URL=${REDIS_URL} - NODE_ID=${NODE_ID} - NODE_TYPE=${NODE_TYPE} - NODE_VERSION=${NODE_VERSION} - NETWORK_MODE=${NETWORK_MODE} - ALLOW_INCOMING_CONNECTIONS=${ALLOW_INCOMING_CONNECTIONS} - SECRET_KEY=${SECRET_KEY} - JWT_SECRET_KEY=${JWT_SECRET_KEY} - ENCRYPTION_KEY=${ENCRYPTION_KEY} - STORAGE_PATH=/app/storage - API_HOST=${API_HOST} - API_PORT=${API_PORT} - DOCKER_SOCK_PATH=/var/run/docker.sock - NODE_PRIVATE_KEY_PATH=/app/keys/node_private_key - NODE_PUBLIC_KEY_PATH=/app/keys/node_public_key - NODE_PUBLIC_KEY_HEX=${NODE_PUBLIC_KEY_HEX} - TELEGRAM_API_KEY=${TELEGRAM_API_KEY} - CLIENT_TELEGRAM_API_KEY=${CLIENT_TELEGRAM_API_KEY} - LOG_LEVEL=${LOG_LEVEL} - LOG_PATH=/app/logs - BOOTSTRAP_CONFIG=${BOOTSTRAP_CONFIG} - MAX_PEER_CONNECTIONS=${MAX_PEER_CONNECTIONS} - SYNC_INTERVAL=${SYNC_INTERVAL} - CONVERT_MAX_PARALLEL=${CONVERT_MAX_PARALLEL} - CONVERT_TIMEOUT=${CONVERT_TIMEOUT} depends_on: - postgres - redis networks: - my-network postgres: image: postgres:15-alpine container_name: my-network-postgres restart: unless-stopped environment: - POSTGRES_DB=${POSTGRES_DB} - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data - ./init_db.sql:/docker-entrypoint-initdb.d/init_db.sql networks: - my-network redis: image: redis:7-alpine container_name: my-network-redis restart: unless-stopped command: redis-server --appendonly yes volumes: - redis_data:/data networks: - my-network volumes: postgres_data: redis_data: networks: my-network: driver: bridge EOF # Создание Dockerfile (минимум системных зависимостей для wheels) cat > Dockerfile << 'EOF' FROM python:3.11-slim WORKDIR /app # Установка системных зависимостей (только необходимые) RUN apt-get update \ && apt-get install -y --no-install-recommends \ gcc \ g++ \ curl \ ffmpeg \ libmagic1 \ && rm -rf /var/lib/apt/lists/* # Копирование requirements и установка Python зависимостей COPY requirements.txt . RUN python -m pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt # Копирование кода приложения COPY app/ ./app/ COPY alembic/ ./alembic/ COPY alembic.ini . COPY bootstrap.json . # Создание директорий RUN mkdir -p /app/storage /app/logs # Права доступа RUN chmod +x /app/app/main.py # Переменные окружения для корректного запуска ENV UVICORN_HOST=0.0.0.0 ENV UVICORN_PORT=8000 ENV API_HOST=0.0.0.0 ENV API_PORT=8000 EXPOSE 8000 CMD ["uvicorn", "app.fastapi_main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] EOF # Создание requirements.txt (пинованные версии для Py3.11, wheels) cat > requirements.txt << 'EOF' fastapi==0.104.1 uvicorn[standard]==0.24.0 pydantic==2.4.2 pydantic-settings==2.0.3 sqlalchemy==2.0.23 alembic==1.12.1 asyncpg==0.29.0 redis==5.0.1 aioredis==2.0.1 aiofiles==23.2.1 aiohttp==3.12.15 yarl==1.17.1 multidict==6.0.5 cryptography==41.0.7 python-jose[cryptography]==3.3.0 python-multipart==0.0.6 httpx==0.25.2 websockets==12.0 docker==6.1.3 base58==2.1.1 passlib[bcrypt]==1.7.4 python-telegram-bot==20.7 APScheduler==3.10.4 psutil==5.9.6 requests==2.31.0 PyYAML==6.0.1 python-dotenv==1.0.0 Pillow==10.1.0 ffmpeg-python==0.2.0 python-magic==0.4.27 jinja2==3.1.2 starlette==0.27.0 structlog==23.2.0 aiogram==3.21.0 magic-filter==1.0.12 sanic==23.12.1 PyJWT==2.8.0 ed25519==1.5 tonsdk==1.0.15 pytonconnect==0.3.2 prometheus-client==0.22.1 pydub==0.25.1 pycryptodome==3.23.0 psycopg2-binary==2.9.10 PyNaCl==1.5.0 uvloop==0.21.0 EOF # Создание init_db.sql cat > init_db.sql << 'EOF' -- MY Network v3.0 Database Initialization -- Extension for UUID generation CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- Create enum types DO $$ BEGIN CREATE TYPE content_status AS ENUM ('pending', 'processing', 'completed', 'failed'); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Create stored_content table (compatible with DEPRECATED-uploader-bot) CREATE TABLE IF NOT EXISTS stored_content ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), hash VARCHAR(255) UNIQUE NOT NULL, original_filename VARCHAR(255) NOT NULL, file_type VARCHAR(100) NOT NULL, file_size BIGINT NOT NULL, content_type VARCHAR(255), storage_path TEXT NOT NULL, decrypted_path TEXT, encrypted_path TEXT NOT NULL, thumbnail_path TEXT, converted_formats JSONB DEFAULT '{}', metadata JSONB DEFAULT '{}', encryption_key TEXT NOT NULL, upload_date TIMESTAMP WITH TIME ZONE DEFAULT NOW(), last_accessed TIMESTAMP WITH TIME ZONE DEFAULT NOW(), access_count INTEGER DEFAULT 0, status content_status DEFAULT 'pending', uploader_id VARCHAR(255), tags TEXT[], description TEXT, is_public BOOLEAN DEFAULT false, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Create indexes for performance CREATE INDEX IF NOT EXISTS idx_stored_content_hash ON stored_content(hash); CREATE INDEX IF NOT EXISTS idx_stored_content_status ON stored_content(status); CREATE INDEX IF NOT EXISTS idx_stored_content_upload_date ON stored_content(upload_date); CREATE INDEX IF NOT EXISTS idx_stored_content_uploader_id ON stored_content(uploader_id); CREATE INDEX IF NOT EXISTS idx_stored_content_file_type ON stored_content(file_type); -- Create nodes table for network management CREATE TABLE IF NOT EXISTS nodes ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), node_id VARCHAR(255) UNIQUE NOT NULL, address INET NOT NULL, port INTEGER NOT NULL, public_key TEXT, node_type VARCHAR(50) NOT NULL, version VARCHAR(20) NOT NULL, last_seen TIMESTAMP WITH TIME ZONE DEFAULT NOW(), trust_score DECIMAL(3,2) DEFAULT 1.0, is_active BOOLEAN DEFAULT true, metadata JSONB DEFAULT '{}', created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Create indexes for nodes CREATE INDEX IF NOT EXISTS idx_nodes_node_id ON nodes(node_id); CREATE INDEX IF NOT EXISTS idx_nodes_address ON nodes(address); CREATE INDEX IF NOT EXISTS idx_nodes_is_active ON nodes(is_active); CREATE INDEX IF NOT EXISTS idx_nodes_last_seen ON nodes(last_seen); -- Create content_sync table for decentralized synchronization CREATE TABLE IF NOT EXISTS content_sync ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), content_hash VARCHAR(255) NOT NULL, node_id VARCHAR(255) NOT NULL, sync_status VARCHAR(50) DEFAULT 'pending', attempts INTEGER DEFAULT 0, last_attempt TIMESTAMP WITH TIME ZONE, error_message TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Create indexes for content_sync CREATE INDEX IF NOT EXISTS idx_content_sync_hash ON content_sync(content_hash); CREATE INDEX IF NOT EXISTS idx_content_sync_node_id ON content_sync(node_id); CREATE INDEX IF NOT EXISTS idx_content_sync_status ON content_sync(sync_status); -- Create conversion_jobs table CREATE TABLE IF NOT EXISTS conversion_jobs ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), content_id UUID REFERENCES stored_content(id), target_format VARCHAR(50) NOT NULL, status content_status DEFAULT 'pending', priority INTEGER DEFAULT 5, attempts INTEGER DEFAULT 0, max_attempts INTEGER DEFAULT 3, error_message TEXT, conversion_params JSONB DEFAULT '{}', output_path TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), started_at TIMESTAMP WITH TIME ZONE, completed_at TIMESTAMP WITH TIME ZONE ); -- Create indexes for conversion_jobs CREATE INDEX IF NOT EXISTS idx_conversion_jobs_content_id ON conversion_jobs(content_id); CREATE INDEX IF NOT EXISTS idx_conversion_jobs_status ON conversion_jobs(status); CREATE INDEX IF NOT EXISTS idx_conversion_jobs_priority ON conversion_jobs(priority); -- Update trigger for updated_at columns CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ language 'plpgsql'; -- Apply update triggers DROP TRIGGER IF EXISTS update_stored_content_updated_at ON stored_content; CREATE TRIGGER update_stored_content_updated_at BEFORE UPDATE ON stored_content FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); DROP TRIGGER IF EXISTS update_nodes_updated_at ON nodes; CREATE TRIGGER update_nodes_updated_at BEFORE UPDATE ON nodes FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); DROP TRIGGER IF EXISTS update_content_sync_updated_at ON content_sync; CREATE TRIGGER update_content_sync_updated_at BEFORE UPDATE ON content_sync FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); DROP TRIGGER IF EXISTS update_conversion_jobs_updated_at ON conversion_jobs; CREATE TRIGGER update_conversion_jobs_updated_at BEFORE UPDATE ON conversion_jobs FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); EOF # Создание alembic.ini cat > alembic.ini << 'EOF' [alembic] script_location = alembic prepend_sys_path = . version_path_separator = os sqlalchemy.url = [post_write_hooks] [loggers] keys = root,sqlalchemy,alembic [handlers] keys = console [formatters] keys = generic [logger_root] level = WARN handlers = console qualname = [logger_sqlalchemy] level = WARN handlers = qualname = sqlalchemy.engine [logger_alembic] level = INFO handlers = qualname = alembic [handler_console] class = StreamHandler args = (sys.stderr,) level = NOTSET formatter = generic [formatter_generic] format = %(levelname)-5.5s [%(name)s] %(message)s datefmt = %H:%M:%S EOF log_success "Файлы проекта созданы" } # Загрузка и настройка проекта setup_project() { clone_repositories create_project_files cd "$PROJECT_DIR/my-network" log_success "Проект настроен в $PROJECT_DIR/my-network" } # Патч приложения: гарантировать регистрацию v3‑роутов patch_app_routes() { log_info "Проверка регистрации роутера /api/v3..." local mfile="$PROJECT_DIR/my-network/app/fastapi_main.py" if [ ! -f "$mfile" ]; then log_warn "fastapi_main.py не найден; пропускаем патч роутов" return 0 fi if ! grep -q "fastapi_v3_routes" "$mfile"; then log_info "Добавляем импорт fastapi_v3_routes и include_router()" # Вставляем импорт рядом с другими импортами роутеров sed -i "/from app.api.fastapi_system_routes/a from app.api.fastapi_v3_routes import router as v3_router" "$mfile" # Вставляем include_router сразу после регистрации node_stats_router sed -i "/app.include_router(node_stats_router)/a \ app.include_router(v3_router) # /api/v3/*" "$mfile" log_success "Роутер /api/v3 зарегистрирован" else log_info "Роутер /api/v3 уже зарегистрирован" fi } # Совместимость с DEPRECATED API v1/system: добавляем compat-роуты и базовую анти-replay защиту add_compatibility_layer() { log_info "Добавление слоя обратной совместимости API..." local app_dir="$PROJECT_DIR/my-network/app" local api_dir="$app_dir/api" mkdir -p "$api_dir" # 1) Создаём fastapi_compat_routes.py с legacy эндпоинтами cat > "$api_dir/fastapi_compat_routes.py" << 'EOF' """ Compatibility routes to preserve deprecated uploader-bot API surface (v1/system). These endpoints mirror legacy paths so older clients continue to function, while new v3 sync API works in parallel. """ import base64 import os from typing import Optional from fastapi import APIRouter, UploadFile, File, HTTPException, Query from fastapi.responses import JSONResponse, StreamingResponse, PlainTextResponse from sqlalchemy import text import aiofiles from app.core.logging import get_logger from app.core.config import get_settings from app.core.database import db_manager from app.core.storage import LocalStorageBackend router = APIRouter(prefix="", tags=["compat-v1"]) logger = get_logger(__name__) settings = get_settings() def _is_table_missing_error(exc: Exception) -> bool: try: msg = str(exc) return 'UndefinedTable' in msg or 'does not exist' in msg or ('relation' in msg and 'does not exist' in msg) except Exception: return False @router.get("/api/system.version") async def system_version(): codebase_hash = os.getenv("CODEBASE_HASH", "unknown") codebase_branch = os.getenv("CODEBASE_BRANCH", os.getenv("GIT_BRANCH", "main")) return {"codebase_hash": codebase_hash, "codebase_branch": codebase_branch} @router.post("/api/system.sendStatus") async def system_send_status(payload: dict): try: message_b58 = payload.get("message") signature = payload.get("signature") if not message_b58 or not signature: raise HTTPException(status_code=400, detail="message and signature required") await logger.ainfo("Compat system.sendStatus", signature=signature) return {"ok": True} except HTTPException: raise except Exception as e: await logger.aerror("sendStatus failed", error=str(e)) raise HTTPException(status_code=500, detail="sendStatus failed") @router.get("/api/tonconnect-manifest.json") async def tonconnect_manifest(): host = str(getattr(settings, "PROJECT_HOST", "")) or os.getenv("PROJECT_HOST", "") or "http://localhost:8000" return { "url": host, "name": "MY Network Node", "iconUrl": f"{host}/static/icon.png", "termsOfUseUrl": f"{host}/terms", "privacyPolicyUrl": f"{host}/privacy", "bridgeUrl": "https://bridge.tonapi.io/bridge", "manifestVersion": 2 } @router.get("/api/platform-metadata.json") async def platform_metadata(): host = str(getattr(settings, "PROJECT_HOST", "")) or os.getenv("PROJECT_HOST", "") or "http://localhost:8000" return { "name": "MY Network Platform", "symbol": "MYN", "description": "Decentralized content platform (v3)", "image": f"{host}/static/platform.png", "external_url": host, "version": "3.0.0" } @router.get("/") async def index_root(): return PlainTextResponse("MY Network Node", status_code=200) @router.get("/favicon.ico") async def favicon(): return PlainTextResponse("", status_code=204) @router.get("/api/v1/node") async def v1_node(): from app.core.crypto import get_ed25519_manager cm = get_ed25519_manager() return {"id": cm.node_id, "node_address": "", "master_address": "", "indexer_height": 0, "services": {}} @router.get("/api/v1/nodeFriendly") async def v1_node_friendly(): from app.core.crypto import get_ed25519_manager cm = get_ed25519_manager() return PlainTextResponse(f"Node ID: {cm.node_id} Indexer height: 0 Services: none ") @router.post("/api/v1/auth.twa") async def v1_auth_twa(payload: dict): user_ref = payload.get("user") or {} token = base64.b64encode(f"twa:{user_ref}".encode()).decode() return {"token": token} @router.get("/api/v1/auth.me") async def v1_auth_me(): return {"user": None, "status": "guest"} @router.post("/api/v1/auth.selectWallet") async def v1_auth_select_wallet(payload: dict): return {"ok": True} @router.get("/api/v1/tonconnect.new") async def v1_tonconnect_new(): return {"ok": True} @router.post("/api/v1/tonconnect.logout") async def v1_tonconnect_logout(payload: dict): return {"ok": True} @router.post("/api/v1/storage") async def v1_storage_upload(file: UploadFile = File(...)): try: data = await file.read() if not data: raise HTTPException(status_code=400, detail="empty file") backend = LocalStorageBackend() from hashlib import sha256 file_hash = sha256(data).hexdigest() file_path = os.path.join(backend.files_path, file_hash) async with aiofiles.open(file_path, 'wb') as f: await f.write(data) return {"hash": file_hash} except HTTPException: raise except Exception as e: await logger.aerror("v1 upload failed", error=str(e)) raise HTTPException(status_code=500, detail="upload failed") @router.get("/api/v1/storage/{file_hash}") async def v1_storage_get(file_hash: str): try: async with db_manager.get_session() as session: result = await session.execute(text("SELECT file_path FROM my_network_content WHERE hash=:h LIMIT 1"), {"h": file_hash}) row = result.first() if not row or not row[0]: raise HTTPException(status_code=404, detail="not found") backend = LocalStorageBackend() return StreamingResponse(backend.get_file_stream(row[0])) except HTTPException: raise except Exception as e: if _is_table_missing_error(e): raise HTTPException(status_code=404, detail="not found") await logger.aerror("v1 storage get failed", error=str(e)) raise HTTPException(status_code=500, detail="failed") @router.get("/api/v1/storage.decodeContentId/{content_id}") async def v1_decode_content_id(content_id: str): try: async with db_manager.get_session() as session: result = await session.execute(text("SELECT id, hash, filename, file_size, mime_type FROM my_network_content WHERE id=:i LIMIT 1"), {"i": content_id}) row = result.first() if not row: raise HTTPException(status_code=404, detail="not found") return {"id": str(row[0]), "hash": row[1], "filename": row[2], "size": row[3], "mime_type": row[4]} except HTTPException: raise except Exception as e: if _is_table_missing_error(e): raise HTTPException(status_code=404, detail="not found") await logger.aerror("decodeContentId failed", error=str(e)) raise HTTPException(status_code=500, detail="failed") @router.get("/api/v1/content.list") async def v1_content_list(limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0)): try: async with db_manager.get_session() as session: result = await session.execute( text("SELECT id, hash, filename, file_size, mime_type FROM my_network_content ORDER BY created_at DESC LIMIT :lim OFFSET :off"), {"lim": limit, "off": offset} ) rows = result.fetchall() or [] return { "items": [ {"id": str(r[0]), "hash": r[1], "filename": r[2], "size": r[3], "mime_type": r[4]} for r in rows ], "limit": limit, "offset": offset } except Exception as e: if _is_table_missing_error(e): return {"items": [], "limit": limit, "offset": offset} await logger.aerror("content.list failed", error=str(e)) raise HTTPException(status_code=500, detail="failed") @router.get("/api/v1/content.view") async def v1_content_view(hash: Optional[str] = None, id: Optional[str] = None): try: if not hash and not id: raise HTTPException(status_code=400, detail="hash or id required") async with db_manager.get_session() as session: if hash: result = await session.execute(text("SELECT id, hash, filename, file_size, mime_type FROM my_network_content WHERE hash=:h LIMIT 1"), {"h": hash}) else: result = await session.execute(text("SELECT id, hash, filename, file_size, mime_type FROM my_network_content WHERE id=:i LIMIT 1"), {"i": id}) row = result.first() if not row: raise HTTPException(status_code=404, detail="not found") return {"id": str(row[0]), "hash": row[1], "filename": row[2], "size": row[3], "mime_type": row[4], "created_at": None} except HTTPException: raise except Exception as e: if _is_table_missing_error(e): raise HTTPException(status_code=404, detail="not found") await logger.aerror("content.view failed", error=str(e)) raise HTTPException(status_code=500, detail="failed") @router.get("/api/v1/content.view/{content_address}") async def v1_content_view_path(content_address: str): try: async with db_manager.get_session() as session: result = await session.execute(text("SELECT id, hash, filename, file_size, mime_type FROM my_network_content WHERE id=:v OR hash=:v LIMIT 1"), {"v": content_address}) row = result.first() if not row: raise HTTPException(status_code=404, detail="not found") return {"id": str(row[0]), "hash": row[1], "filename": row[2], "size": row[3], "mime_type": row[4], "created_at": None} except HTTPException: raise except Exception as e: if _is_table_missing_error(e): raise HTTPException(status_code=404, detail="not found") await logger.aerror("content.view(path) failed", error=str(e)) raise HTTPException(status_code=500, detail="failed") @router.get("/api/v1/content.friendlyList") async def v1_content_friendly_list(limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0)): return await v1_content_list(limit, offset) @router.get("/api/v1.5/content.list") async def v1_5_content_list(limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0)): return await v1_content_list(limit, offset) @router.post("/api/v1/blockchain.sendNewContentMessage") async def v1_chain_send_new_content(payload: dict): await logger.ainfo("compat blockchain.sendNewContentMessage", payload=payload) return {"ok": True} @router.post("/api/v1/blockchain.sendPurchaseContent") async def v1_chain_send_purchase(payload: dict): await logger.ainfo("compat blockchain.sendPurchaseContent", payload=payload) return {"ok": True} @router.post("/api/v1/blockchain.sendPurchaseContentMessage") async def v1_chain_send_purchase_message(payload: dict): await logger.ainfo("compat blockchain.sendPurchaseContentMessage", payload=payload) return {"ok": True} @router.get("/api/v1/account") async def v1_account(): return {"ok": True} EOF # 2) Регистрируем compat-роуты в FastAPI local fm="$app_dir/fastapi_main.py" if [ -f "$fm" ]; then if ! grep -q "fastapi_compat_routes" "$fm"; then sed -i "/from app.api.fastapi_system_routes/a from app.api.fastapi_compat_routes import router as compat_router" "$fm" sed -i "/app.include_router(node_stats_router)/a \ app.include_router(compat_router) # legacy compat routes" "$fm" fi fi # 3) Базовая анти‑replay защита в межузловых запросах local fnr="$api_dir/fastapi_node_routes.py" if [ -f "$fnr" ]; then # Импорт кэша if ! grep -q "get_cache_manager" "$fnr"; then sed -i "/from app.core.logging/a from app.core.database import get_cache_manager" "$fnr" fi # Вставка проверки nonce/timestamp после разбора message_data sed -i "/message_data = json.loads/a \ # Anti-replay: validate timestamp and nonce\n try:\n ts = message_data.get(\"timestamp\")\n nonce = message_data.get(\"nonce\")\n if ts:\n from datetime import datetime, timezone\n now = datetime.now(timezone.utc).timestamp()\n if abs(float(ts) - float(now)) > 300:\n raise HTTPException(status_code=400, detail=\"stale timestamp\")\n if nonce:\n cache = await get_cache_manager()\n cache_key = f\"replay:{node_id}:{nonce}\"\n if await cache.get(cache_key):\n raise HTTPException(status_code=400, detail=\"replay detected\")\n await cache.set(cache_key, True, ttl=600)\n except Exception as _e:\n # For backward compatibility, do not fail hard if fields missing\n pass" "$fnr" fi } # Функция для проверки доступности Docker registry check_docker_registry() { log_info "Проверка доступности Docker registry..." # Проверяем доступность registry.docker.io if timeout 30 curl -s --connect-timeout 10 https://registry.docker.io/v2/ >/dev/null 2>&1; then log_success "Docker registry доступен" return 0 else log_warn "Docker registry недоступен или медленно отвечает" return 1 fi } # Функция для настройки Docker daemon timeout configure_docker_timeout() { log_info "Настройка Docker timeout для сетевых операций..." # Создаем или обновляем Docker daemon config local docker_config="/etc/docker/daemon.json" local temp_config="/tmp/daemon.json.tmp" if [ -f "$docker_config" ]; then # Читаем существующий конфиг cp "$docker_config" "$temp_config" else # Создаем новый конфиг echo '{}' > "$temp_config" fi # Добавляем настройки timeout с помощью jq если доступен if command -v jq >/dev/null 2>&1; then jq '. + { "registry-mirrors": [], "insecure-registries": [], "max-concurrent-downloads": 3, "max-concurrent-uploads": 3, "default-runtime": "runc" }' "$temp_config" > "${temp_config}.new" && mv "${temp_config}.new" "$temp_config" if cp "$temp_config" "$docker_config" 2>/dev/null; then log_info "Docker daemon конфигурация обновлена" # Перезапускаем Docker только если это безопасно if ! docker ps >/dev/null 2>&1 || [ "$(docker ps -q | wc -l)" -eq 0 ]; then systemctl reload docker 2>/dev/null || true fi fi fi rm -f "$temp_config" 2>/dev/null } # Сборка converter образа build_converter_image() { log_info "🔧 Сборка converter образа из converter-module..." cd "$PROJECT_DIR/my-network" # Проверяем наличие клонированного converter-module if [ ! -d "converter-module" ]; then log_error "converter-module не найден. Проверьте клонирование репозиториев." return 1 fi cd converter-module # Проверяем наличие Dockerfile в папке converter if [ ! -f "converter/Dockerfile" ]; then log_error "Dockerfile не найден в converter-module/converter/" return 1 fi # Переходим в папку converter для сборки cd converter # Настраиваем Docker timeout configure_docker_timeout # Проверяем доступность registry if ! check_docker_registry; then log_warn "Docker registry недоступен, пробуем продолжить с увеличенным timeout" fi # Сборка converter образа из оригинального репозитория с retry логикой log_info "Сборка Docker образа для converter..." # Попытки сборки с retry local max_attempts=3 local attempt=1 local success=false while [ $attempt -le $max_attempts ] && [ "$success" = false ]; do log_info "Попытка сборки $attempt из $max_attempts..." # Сборка с увеличенными таймаутами и дополнительными параметрами if docker build \ --network=host \ --build-arg BUILDKIT_PROGRESS=plain \ --build-arg HTTP_TIMEOUT=300 \ --build-arg HTTPS_TIMEOUT=300 \ -t my-network-converter:latest . ; then log_success "Converter образ собран: my-network-converter:latest" success=true else log_warn "Попытка $attempt неудачна" if [ $attempt -lt $max_attempts ]; then log_info "Ожидание 15 секунд перед следующей попыткой..." sleep 15 # Очистка Docker build cache и системы при неудачной попытке log_info "Очистка Docker cache..." docker builder prune -f >/dev/null 2>&1 || true docker system prune -f >/dev/null 2>&1 || true # Попытка сброса сетевых настроек Docker if [ $attempt -eq 2 ]; then log_info "Перезапуск Docker daemon для сброса сетевых настроек..." systemctl restart docker >/dev/null 2>&1 || true sleep 10 fi fi attempt=$((attempt + 1)) fi done if [ "$success" = false ]; then log_error "Не удалось собрать converter образ после $max_attempts попыток" log_warn "Возможные причины:" log_warn "1. Проблемы с подключением к Docker Hub" log_warn "2. Сетевые проблемы на сервере" log_warn "3. Временные проблемы Docker Registry" log_info "Установка продолжится без converter образа" log_info "Converter можно собрать позже командой:" log_info "cd $PROJECT_DIR/my-network/converter-module/converter && docker build -t my-network-converter:latest ." fi cd "$PROJECT_DIR/my-network" } # Установка и настройка nginx setup_nginx() { if [ "$ENABLE_WEB_CLIENT" = "true" ] || [ "$ENABLE_SSL" = "true" ]; then log_info "🌐 Установка и настройка nginx..." # Установка nginx case $OS_ID in ubuntu|debian) export DEBIAN_FRONTEND=noninteractive export NEEDRESTART_MODE=a export NEEDRESTART_SUSPEND=1 apt-get update -qq apt-get install -y -qq -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" \ nginx certbot python3-certbot-nginx unset DEBIAN_FRONTEND unset NEEDRESTART_MODE unset NEEDRESTART_SUSPEND ;; centos|rhel|fedora) if command -v dnf &> /dev/null; then dnf install -y -q nginx certbot python3-certbot-nginx else yum install -y -q nginx certbot python3-certbot-nginx fi ;; esac # Развертывание web2-client if [ "$ENABLE_WEB_CLIENT" = "true" ]; then log_info "Развертывание web2-client из репозитория..." # Проверяем наличие клонированного web2-client if [ ! -d "$PROJECT_DIR/my-network/web2-client" ]; then log_error "web2-client не найден. Проверьте клонирование репозиториев." return 1 fi # Создаем директорию для nginx mkdir -p /var/www/my-network-web # Копируем файлы web2-client cd "$PROJECT_DIR/my-network/web2-client" # Если есть сборка (build process), выполняем её if [ -f "package.json" ]; then log_info "Установка зависимостей web2-client..." if has_cmd npm; then npm install || log_warn "Не удалось установить зависимости npm" else log_warn "npm не установлен; пропускаем установку зависимостей" fi # Если есть build скрипт, выполняем сборку if has_cmd npm && npm run build 2>/dev/null; then log_success "Сборка web2-client завершена" # Копируем собранные файлы if [ -d "build" ]; then cp -r build/* /var/www/my-network-web/ elif [ -d "dist" ]; then cp -r dist/* /var/www/my-network-web/ else cp -r . /var/www/my-network-web/ fi else log_warn "Сборка не требуется, копируем исходные файлы" cp -r . /var/www/my-network-web/ fi else # Копируем файлы как есть log_info "Копирование статических файлов web2-client..." cp -r . /var/www/my-network-web/ fi # Настройка прав доступа chown -R www-data:www-data /var/www/my-network-web/ chmod -R 755 /var/www/my-network-web/ log_success "Web2-client развернут в /var/www/my-network-web" fi # Полная очистка старых nginx конфигураций log_info "Очистка старых nginx конфигураций..." # Удаляем все существующие конфигурации sites rm -f /etc/nginx/sites-enabled/* 2>/dev/null || true rm -f /etc/nginx/sites-available/my-network* 2>/dev/null || true # Очистка конфигураций certbot в nginx.conf if [ -f /etc/nginx/nginx.conf.backup ]; then cp /etc/nginx/nginx.conf.backup /etc/nginx/nginx.conf else cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.backup fi # Удаляем все SSL включения из основного nginx.conf sed -i '/# managed by Certbot/d' /etc/nginx/nginx.conf 2>/dev/null || true sed -i '/ssl_certificate/d' /etc/nginx/nginx.conf 2>/dev/null || true sed -i '/ssl_certificate_key/d' /etc/nginx/nginx.conf 2>/dev/null || true sed -i '/ssl_dhparam/d' /etc/nginx/nginx.conf 2>/dev/null || true # Создание чистой HTTP конфигурации nginx log_info "Создание чистой HTTP конфигурации nginx..." cat > /etc/nginx/sites-available/my-network << EOF # MY Network v3.0 nginx configuration # Upstream для API upstream my_network_api { server 127.0.0.1:8000; } server { listen 80; server_name ${DOMAIN:-localhost}; # Максимальный размер для chunked uploads client_max_body_size 10G; client_body_timeout 300s; client_header_timeout 300s; # Proxy buffering для больших файлов proxy_buffering off; proxy_request_buffering off; proxy_max_temp_file_size 0; # Статический контент (веб-интерфейс) location / { $([ "$ENABLE_WEB_CLIENT" = "true" ] && echo " root /var/www/my-network-web;" || echo " return 404;") $([ "$ENABLE_WEB_CLIENT" = "true" ] && echo " index index.html;" || echo "") $([ "$ENABLE_WEB_CLIENT" = "true" ] && echo " try_files \$uri \$uri/ =404;" || echo "") } # API proxy location /api/ { proxy_pass http://my_network_api; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; # Для chunked uploads proxy_http_version 1.1; proxy_set_header Connection ""; proxy_buffering off; proxy_cache off; # Таймауты для больших файлов proxy_connect_timeout 300s; proxy_send_timeout 300s; proxy_read_timeout 300s; } # Health check location /health { proxy_pass http://my_network_api; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } # Мониторинг location /monitor { proxy_pass http://my_network_api; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } } EOF # Активация HTTP конфигурации if [ ! -L "/etc/nginx/sites-enabled/my-network" ]; then ln -s /etc/nginx/sites-available/my-network /etc/nginx/sites-enabled/ fi # Тест конфигурации nginx log_info "Тестирование HTTP конфигурации nginx..." if nginx -t; then log_success "HTTP конфигурация nginx корректна" else log_error "Ошибка в HTTP конфигурации nginx" log_info "Показать детали ошибки nginx:" nginx -t 2>&1 || true return 1 fi # Запуск nginx с HTTP конфигурацией systemctl enable nginx systemctl restart nginx # Проверяем, что nginx запустился успешно sleep 3 if systemctl is-active nginx >/dev/null 2>&1; then log_success "Nginx запущен с HTTP конфигурацией" else log_error "Nginx не запустился" systemctl status nginx || true return 1 fi # Настройка SSL если нужно if [ "$ENABLE_SSL" = "true" ] && [ -n "$DOMAIN" ] && [ -n "$EMAIL" ]; then install_ssl_certificates fi else log_info "Nginx пропущен (веб-клиент и SSL отключены)" fi } # Установка SSL сертификатов install_ssl_certificates() { log_info "🔒 Установка SSL сертификата для $DOMAIN..." # Проверка DNS записи if ! host "$DOMAIN" > /dev/null 2>&1; then log_warn "DNS запись для $DOMAIN не найдена" log_info "Убедитесь что домен указывает на этот сервер" if check_interactive; then echo -n "Продолжить установку SSL? [y/N]: " >&2 read -r ssl_continue < /dev/tty if [[ ! $ssl_continue =~ ^[Yy]$ ]]; then log_info "Установка SSL пропущена" return 0 fi else log_info "Неинтерактивный режим: пропускаем SSL (требует ручной настройки)" return 0 fi fi # Проверка и остановка запущенных процессов certbot log_info "Проверка запущенных процессов certbot..." if pgrep -f certbot > /dev/null; then log_warn "Обнаружен запущенный процесс certbot, завершаем..." pkill -f certbot 2>/dev/null || true sleep 5 fi # Очистка временных файлов certbot rm -rf /tmp/tmp*/log 2>/dev/null || true # Проверка существующих сертификатов и их очистка при конфликте log_info "Проверка существующих сертификатов..." if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then log_warn "Обнаружен существующий сертификат для $DOMAIN, удаляем для предотвращения конфликтов..." certbot delete --cert-name "$DOMAIN" --non-interactive 2>/dev/null || true sleep 2 fi # Убеждаемся что nginx работает с HTTP перед установкой SSL log_info "Проверка готовности nginx для SSL..." if ! systemctl is-active nginx >/dev/null 2>&1 || ! nginx -t 2>/dev/null; then log_error "Nginx не готов для SSL установки" log_info "Система продолжит работу без SSL" return 0 fi # Получение сертификата через certbot с явным указанием типа ключа log_info "Запуск certbot для получения SSL сертификата..." if certbot --nginx -d "$DOMAIN" --email "$EMAIL" --agree-tos --non-interactive --redirect --key-type rsa --cert-name "$DOMAIN"; then log_success "SSL сертификат установлен для $DOMAIN" # Настройка автообновления if ! crontab -l 2>/dev/null | grep -q "certbot renew"; then (crontab -l 2>/dev/null; echo "0 12 * * * /usr/bin/certbot renew --quiet") | crontab - log_success "Автообновление SSL настроено" fi # Обновление firewall для HTTPS case $OS_ID in ubuntu|debian) ufw allow 443/tcp 2>/dev/null || true ;; centos|rhel|fedora) firewall-cmd --permanent --add-service=https 2>/dev/null || true firewall-cmd --reload 2>/dev/null || true ;; esac log_success "HTTPS порт открыт в firewall" else log_error "Ошибка установки SSL сертификата" log_warn "Возможные причины:" log_warn "1. DNS запись $DOMAIN не указывает на этот сервер" log_warn "2. Порт 80 заблокирован или недоступен из интернета" log_warn "3. Nginx не запущен или неправильно настроен" log_warn "4. Достигнут лимит запросов Let's Encrypt" log_warn "5. Другой процесс certbot уже запущен" log_info "Система продолжит работу без SSL. SSL можно настроить позже вручную:" log_info "certbot --nginx -d $DOMAIN --email $EMAIL --agree-tos --non-interactive --redirect" # НЕ завершаем скрипт, SSL не критичен для работы системы fi } # Генерация конфигурации generate_config() { log_info "⚙️ Генерация конфигурации..." # Генерация ed25519 ключей для ноды log_info "Генерация ed25519 ключей для ноды..." # Создаем временную папку для ключей mkdir -p "$CONFIG_DIR/keys" PRIVATE_KEY_FILE="$CONFIG_DIR/keys/node_private_key" PUBLIC_KEY_FILE="$CONFIG_DIR/keys/node_public_key" local python_keys_json="" if command -v python3 >/dev/null 2>&1; then if [ -f "$PWD/scripts/generate_node_keys.py" ]; then log_info "Пробуем сгенерировать ключи через Python (cryptography)..." python_keys_json=$(python3 "$PWD/scripts/generate_node_keys.py" 2>/dev/null || echo "") elif [ -f "$PROJECT_DIR/my-network/scripts/generate_node_keys.py" ]; then log_info "Пробуем сгенерировать ключи через Python из каталога проекта..." python_keys_json=$(python3 "$PROJECT_DIR/my-network/scripts/generate_node_keys.py" 2>/dev/null || echo "") fi fi if [ -n "$python_keys_json" ]; then log_success "Ключи успешно сгенерированы Python-скриптом" PRIVATE_KEY_PEM=$(echo "$python_keys_json" | jq -r '.private_key_pem' 2>/dev/null || echo "") PUBLIC_KEY_PEM=$(echo "$python_keys_json" | jq -r '.public_key_pem' 2>/dev/null || echo "") PUBLIC_KEY_HEX=$(echo "$python_keys_json" | jq -r '.public_key_hex' 2>/dev/null || echo "") NODE_ID=$(echo "$python_keys_json" | jq -r '.node_id' 2>/dev/null || echo "") echo "$PRIVATE_KEY_PEM" > "$PRIVATE_KEY_FILE" echo "$PUBLIC_KEY_PEM" > "$PUBLIC_KEY_FILE" else log_warn "Python-генерация недоступна, fallback на OpenSSL" openssl genpkey -algorithm ed25519 -out "$PRIVATE_KEY_FILE" openssl pkey -in "$PRIVATE_KEY_FILE" -pubout -out "$PUBLIC_KEY_FILE" PUBLIC_KEY_HEX=$(openssl pkey -in "$PRIVATE_KEY_FILE" -pubout -outform DER | tail -c 32 | xxd -p -c 32) NODE_ID="node-$(echo "$PUBLIC_KEY_HEX" | cut -c1-16)" PRIVATE_KEY_PEM=$(cat "$PRIVATE_KEY_FILE") PUBLIC_KEY_PEM=$(cat "$PUBLIC_KEY_FILE") fi log_success "Ed25519 ключи подготовлены: NODE_ID=$NODE_ID" # Генерация других ключей SECRET_KEY=$(openssl rand -hex 32) JWT_SECRET_KEY=$(openssl rand -hex 32) ENCRYPTION_KEY=$(openssl rand -hex 32) DB_PASSWORD=$(openssl rand -hex 16) # Создание .env файла cat > "$CONFIG_DIR/.env" << EOF # MY Network v3.0 Configuration # Generated: $(date) # Node Configuration NODE_ID=$NODE_ID NODE_TYPE=$NODE_TYPE NODE_VERSION=$NODE_VERSION NETWORK_MODE=$NETWORK_MODE ALLOW_INCOMING_CONNECTIONS=$ALLOW_INCOMING # Database Configuration DATABASE_URL=postgresql+asyncpg://myuser:$DB_PASSWORD@postgres:5432/mynetwork POSTGRES_DB=mynetwork POSTGRES_USER=myuser POSTGRES_PASSWORD=$DB_PASSWORD # Redis Configuration REDIS_URL=redis://redis:6379/0 # Security SECRET_KEY=$SECRET_KEY JWT_SECRET_KEY=$JWT_SECRET_KEY ENCRYPTION_KEY=$ENCRYPTION_KEY # Storage STORAGE_PATH=$STORAGE_DIR # API Configuration API_HOST=0.0.0.0 API_PORT=8000 UVICORN_HOST=0.0.0.0 UVICORN_PORT=8000 FASTAPI_HOST=0.0.0.0 FASTAPI_PORT=8000 # Docker Configuration DOCKER_SOCK_PATH=$DOCKER_SOCK_PATH # Node Cryptographic Keys NODE_PRIVATE_KEY_PATH=$CONFIG_DIR/keys/node_private_key NODE_PUBLIC_KEY_PATH=$CONFIG_DIR/keys/node_public_key NODE_PUBLIC_KEY_HEX=$PUBLIC_KEY_HEX # Telegram Bots TELEGRAM_API_KEY=$TELEGRAM_API_KEY CLIENT_TELEGRAM_API_KEY=$CLIENT_TELEGRAM_API_KEY # Logging LOG_LEVEL=INFO LOG_PATH=$LOGS_DIR # Network Configuration BOOTSTRAP_CONFIG=$BOOTSTRAP_CONFIG MAX_PEER_CONNECTIONS=50 SYNC_INTERVAL=300 # Converter Configuration CONVERT_MAX_PARALLEL=3 CONVERT_TIMEOUT=300 EOF # Создание/обновление bootstrap.json if [ "$BOOTSTRAP_CONFIG" = "new" ]; then cat > "$CONFIG_DIR/bootstrap.json" << EOF { "version": "$NODE_VERSION", "network_id": "my-network-$(date +%s)", "created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "bootstrap_nodes": [ { "id": "$NODE_ID", "node_id": "$NODE_ID", "address": "$(curl -s ifconfig.me || echo 'localhost')", "port": 8000, "public_key": "$PUBLIC_KEY_HEX", "trusted": true, "node_type": "bootstrap" } ], "network_settings": { "protocol_version": "3.0", "max_peers": 50, "sync_interval": 300, "individual_decisions": true, "no_consensus": true } } EOF log_success "Создан новый bootstrap.json (Bootstrap режим)" elif [ "$BOOTSTRAP_CONFIG" != "default" ]; then cp "$BOOTSTRAP_CONFIG" "$CONFIG_DIR/bootstrap.json" log_success "Скопирован кастомный bootstrap.json" else log_info "Используется дефолтная конфигурация bootstrap.json из проекта" fi # Копирование конфигурации в проект cp "$CONFIG_DIR/.env" "$PROJECT_DIR/my-network/.env" if [ -f "$CONFIG_DIR/bootstrap.json" ]; then cp "$CONFIG_DIR/bootstrap.json" "$PROJECT_DIR/my-network/bootstrap.json" fi # Копирование ключей в проект mkdir -p "$PROJECT_DIR/my-network/config/keys" cp "$CONFIG_DIR/keys/node_private_key" "$PROJECT_DIR/my-network/config/keys/" cp "$CONFIG_DIR/keys/node_public_key" "$PROJECT_DIR/my-network/config/keys/" # Защита приватного ключа chmod 600 "$PROJECT_DIR/my-network/config/keys/node_private_key" chmod 644 "$PROJECT_DIR/my-network/config/keys/node_public_key" log_success "Конфигурация сгенерирована" } # Настройка firewall setup_firewall() { if [ "$ALLOW_INCOMING" = "true" ]; then log_info "🔥 Настройка firewall для публичной ноды..." case $OS_ID in ubuntu|debian) # UFW для Ubuntu/Debian ufw default deny incoming ufw default allow outgoing ufw allow ssh ufw allow 8000/tcp # API порт MY Network (FastAPI) ufw --force enable ;; centos|rhel|fedora) # Firewalld для CentOS/RHEL/Fedora systemctl start firewalld systemctl enable firewalld firewall-cmd --permanent --add-service=ssh firewall-cmd --permanent --add-port=8000/tcp firewall-cmd --reload ;; esac log_success "Firewall настроен (порт 8000 открыт)" else log_info "Приватная нода - firewall настройка пропущена" fi } # Сборка и запуск контейнеров build_and_start() { log_info "🐳 Сборка и запуск MY Network v3.0..." cd "$PROJECT_DIR/my-network" # Остановка существующих контейнеров init_compose_cmd $COMPOSE_CMD down 2>/dev/null || true # Сборка образов log_info "Сборка Docker образов..." if ! $COMPOSE_CMD config >/dev/null 2>&1; then log_error "docker-compose.yml содержит ошибки. Проверьте конфигурацию и .env" $COMPOSE_CMD config || true exit 1 fi $COMPOSE_CMD build --no-cache # Запуск сервисов log_info "Запуск сервисов..." $COMPOSE_CMD up -d # Ожидание готовности сервисов log_info "Ожидание готовности сервисов..." sleep 30 # Проверка статуса контейнеров if $COMPOSE_CMD ps | grep -q "Up"; then log_success "Контейнеры запущены" else log_error "Ошибка запуска контейнеров" $COMPOSE_CMD logs || true exit 1 fi } # Инициализация базы данных init_database() { log_info "🗄️ Инициализация базы данных..." cd "$PROJECT_DIR/my-network" # Ожидание готовности PostgreSQL log_info "Ожидание готовности PostgreSQL..." for i in {1..30}; do if $COMPOSE_CMD exec -T postgres pg_isready -U myuser -d mynetwork > /dev/null 2>&1; then break fi echo -n "." sleep 2 done echo "" # Выполнение миграций отключено из-за несовместимости моделей # База данных инициализируется через init_db.sql в PostgreSQL контейнере log_info "База данных инициализируется автоматически через init_db.sql" # Проверяем что база данных готова log_info "Проверка готовности базы данных..." if $COMPOSE_CMD exec -T postgres psql -U myuser -d mynetwork -c "SELECT 1;" > /dev/null 2>&1; then log_success "База данных инициализирована" else log_error "Ошибка доступа к базе данных" return 1 fi } # Подключение к сети connect_to_network() { log_info "🌐 Подключение к MY Network..." cd "$PROJECT_DIR/my-network" # Ожидание готовности API log_info "Ожидание готовности API..." for i in {1..60}; do if curl -f "http://localhost:8000/health" > /dev/null 2>&1; then break fi echo -n "." sleep 2 done echo "" if ! curl -f "http://localhost:8000/health" > /dev/null 2>&1; then log_error "API недоступно" return 1 fi log_success "API готово: http://localhost:8000" # Статистика ноды log_info "Получение статистики ноды..." node_stats=$(curl -s "http://localhost:8000/api/v3/node/status" 2>/dev/null || echo "{}") echo "$node_stats" | jq '.' 2>/dev/null || echo "Статистика недоступна" # Подключение к bootstrap нодам (если не bootstrap) if [ "$NODE_TYPE" != "bootstrap" ]; then log_info "Попытка подключения к bootstrap нодам..." # Автообнаружение пиров curl -X POST "http://localhost:8000/api/v3/node/connect" \ -H "Content-Type: application/json" \ -d '{"auto_discover": true}' > /dev/null 2>&1 sleep 10 else log_info "Bootstrap нода готова принимать подключения" fi # Базовая статистика пиров peers_response=$(curl -s "http://localhost:8000/api/v3/node/peers" 2>/dev/null || echo '{"count": 0}') peer_count=$(echo "$peers_response" | jq -r '.count // 0' 2>/dev/null || echo "0") if [ "$peer_count" -gt 0 ]; then log_success "Подключено к $peer_count пир(ам)" else log_warn "Пока не удалось подключиться к другим нодам" log_info "Нода будет продолжать попытки подключения в фоне" fi # Расширенная диагностика сети и совместимости версий (graceful) if command -v python3 >/dev/null 2>&1; then diag_script="" if [ -f "$PWD/scripts/check_network_connectivity.py" ]; then diag_script="$PWD/scripts/check_network_connectivity.py" elif [ -f "$PROJECT_DIR/my-network/scripts/check_network_connectivity.py" ]; then diag_script="$PROJECT_DIR/my-network/scripts/check_network_connectivity.py" fi if [ -n "$diag_script" ]; then log_info "Выполняем расширенную проверку сетевой связности и версий..." diag_json=$(API_BASE="http://localhost:8000" TEST_PEERS="true" python3 "$diag_script" 2>/dev/null || echo "") if [ -n "$diag_json" ]; then total_peers=$(echo "$diag_json" | jq -r '.peers.count_reported // 0' 2>/dev/null || echo "0") tested_ok=$(echo "$diag_json" | jq -r '[.peers.tested[] | select(.ok==true)] | length' 2>/dev/null || echo "0") tested_fail=$(echo "$diag_json" | jq -r '[.peers.tested[] | select(.ok!=true)] | length' 2>/dev/null || echo "0") mismatches=$(echo "$diag_json" | jq -r '.network.version_mismatch | length' 2>/dev/null || echo "0") log_info "📊 Итог подключения:" log_info " • Всего пиров (по отчёту API): $total_peers" log_info " • Успешных активных проверок: $tested_ok" log_info " • Неуспешных проверок: $tested_fail" log_info " • Несовпадений версий: $mismatches" if [ "$mismatches" -gt 0 ]; then echo "$diag_json" | jq -r '.network.version_mismatch' 2>/dev/null || true fi else log_warn "Расширенная диагностика не вернула данных" fi fi fi # Статистика сети network_stats=$(curl -s "http://localhost:8000/api/v3/network/stats" 2>/dev/null || echo '{}') log_info "Статистика сети:" echo "$network_stats" | jq '.' 2>/dev/null || echo "Статистика сети недоступна" # Инициализация подсистемы валидации (graceful) log_info "Инициализация системы валидации (если поддерживается API)..." curl -s -X POST "http://localhost:8000/api/v3/validation/init" -H "Content-Type: application/json" -d '{}' >/dev/null 2>&1 || true val_status=$(curl -s "http://localhost:8000/api/v3/validation/status" 2>/dev/null || echo '{}') if [ -n "$val_status" ]; then log_success "Статус валидации:" echo "$val_status" | jq '.' 2>/dev/null || echo "$val_status" else log_info "Эндпоинты валидации недоступны, пропускаем" fi # Проверка подсистемы децентрализованной статистики (graceful) log_info "Проверка подсистемы статистики..." curl -s "http://localhost:8000/api/my/monitor" >/dev/null 2>&1 && log_success "Мониторинг доступен: /api/my/monitor" || log_warn "Мониторинг недоступен" curl -s "http://localhost:8000/api/v3/stats/metrics" >/dev/null 2>&1 && log_success "Метрики доступны: /api/v3/stats/metrics" || log_warn "Метрики недоступны" } # Создание systemd сервиса create_systemd_service() { log_info "⚙️ Создание systemd сервиса..." # Определяем команду compose для systemd local compose_service_cmd if has_cmd docker && docker compose version >/dev/null 2>&1; then compose_service_cmd="docker compose" elif has_cmd docker-compose; then compose_service_cmd="/usr/local/bin/docker-compose" else compose_service_cmd="docker compose" # по умолчанию fi # Создаем unit с усиленными зависимостями и префлайт-проверками cat > /etc/systemd/system/my-network.service << EOF [Unit] Description=MY Network v3.0 Node Wants=network-online.target After=network-online.target docker.service Requires=docker.service [Service] Type=oneshot RemainAfterExit=yes WorkingDirectory=$PROJECT_DIR/my-network Environment=COMPOSE_PROJECT_NAME=my-network ExecStartPre=/bin/sh -lc 'which docker >/dev/null' ExecStartPre=/bin/sh -lc "$compose_service_cmd version" ExecStartPre=/bin/sh -lc "$compose_service_cmd -f docker-compose.yml config -q" ExecStartPre=/bin/sh -lc "$compose_service_cmd pull --quiet || true" ExecStart=/bin/sh -lc "$compose_service_cmd up -d" ExecStop=/bin/sh -lc "$compose_service_cmd down" TimeoutStartSec=300 TimeoutStopSec=120 User=root [Install] WantedBy=multi-user.target EOF # Активация и немедленный запуск systemctl daemon-reload systemctl enable --now my-network || true # Верификация запуска if systemctl is-active my-network >/dev/null 2>&1; then log_success "Systemd сервис создан, активирован и запущен" else log_warn "Systemd сервис создан, но не активен. Проверяем логи и пробуем запустить..." systemctl start my-network || true fi } # Финальный отчет final_report() { clear echo -e "${GREEN}" cat << "EOF" ╔══════════════════════════════════════════════════════════════╗ ║ УСТАНОВКА ЗАВЕРШЕНА! ║ ║ MY Network v3.0 ║ ╚══════════════════════════════════════════════════════════════╝ EOF echo -e "${NC}" echo "" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE} СТАТУС СИСТЕМЫ ${NC}" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" # Проверка сервисов cd "$PROJECT_DIR/my-network" echo "" echo -e "${WHITE}🐳 Docker контейнеры:${NC}" init_compose_cmd if $COMPOSE_CMD ps --format "table {{.Name}}\t{{.State}}\t{{.Ports}}" >/dev/null 2>&1; then $COMPOSE_CMD ps --format "table {{.Name}}\t{{.State}}\t{{.Ports}}" else $COMPOSE_CMD ps || true fi echo "" echo -e "${WHITE}⚙️ Systemd сервис:${NC}" if systemctl is-active my-network >/dev/null 2>&1; then echo -e " ${GREEN}✅ my-network: активен${NC}" else echo -e " ${RED}❌ my-network: неактивен${NC}" fi echo "" echo -e "${WHITE}🌐 Сетевая конфигурация:${NC}" echo -e " Тип ноды: ${YELLOW}$NODE_TYPE${NC}" echo -e " Режим сети: ${YELLOW}$NETWORK_MODE${NC}" echo -e " Входящие соединения: ${YELLOW}$ALLOW_INCOMING${NC}" if [ "$ALLOW_INCOMING" = "true" ]; then PUBLIC_IP=$(curl -s ifconfig.me 2>/dev/null || echo "неизвестен") echo -e " Публичный IP: ${YELLOW}$PUBLIC_IP${NC}" if (has_cmd ss && ss -ltnp 2>/dev/null | grep -q ":8000 ") || (has_cmd netstat && netstat -tlnp 2>/dev/null | grep -q ":8000 "); then echo -e " API порт 8000: ${GREEN}✅ открыт${NC}" else echo -e " API порт 8000: ${RED}❌ недоступен${NC}" fi else echo -e " Режим: ${YELLOW}Приватная нода (только исходящие)${NC}" fi echo "" echo -e "${WHITE}📡 API и интерфейсы:${NC}" if curl -f "http://localhost:8000/health" > /dev/null 2>&1; then echo -e " API: ${GREEN}✅ http://localhost:8000${NC}" echo -e " Health: ${GREEN}✅ http://localhost:8000/health${NC}" echo -e " Мониторинг: ${GREEN}✅ http://localhost:8000/api/my/monitor/${NC}" echo -e " Статус ноды: ${GREEN}✅ http://localhost:8000/api/v3/node/status${NC}" else echo -e " API: ${RED}❌ недоступно${NC}" fi # Веб-клиент и SSL информация if [ "$ENABLE_WEB_CLIENT" = "true" ]; then echo "" echo -e "${WHITE}🌐 Веб-интерфейс:${NC}" if systemctl is-active nginx >/dev/null 2>&1; then if [ "$ENABLE_SSL" = "true" ] && [ -n "$DOMAIN" ]; then echo -e " Веб-интерфейс: ${GREEN}✅ https://$DOMAIN${NC}" echo -e " SSL сертификат: ${GREEN}✅ активен для $DOMAIN${NC}" echo -e " HTTP redirect: ${GREEN}✅ автоматический переход на HTTPS${NC}" else if [ "$ALLOW_INCOMING" = "true" ]; then PUBLIC_IP=$(curl -s ifconfig.me 2>/dev/null || echo "localhost") echo -e " Веб-интерфейс: ${GREEN}✅ http://$PUBLIC_IP${NC}" else echo -e " Веб-интерфейс: ${GREEN}✅ http://localhost${NC}" fi echo -e " SSL: ${YELLOW}⚠ не настроен${NC}" fi echo -e " Nginx: ${GREEN}✅ работает${NC}" echo -e " Chunked upload: ${GREEN}✅ поддерживается (до 10GB)${NC}" else echo -e " Веб-интерфейс: ${RED}❌ Nginx не запущен${NC}" fi else echo -e " Веб-интерфейс: ${YELLOW}⚠ отключен${NC}" fi echo "" echo -e "${WHITE}🤖 Telegram боты:${NC}" if [ -n "$TELEGRAM_API_KEY" ]; then echo -e " Основной бот: ${GREEN}✅ настроен${NC}" else echo -e " Основной бот: ${YELLOW}⚠ отключен${NC}" fi if [ -n "$CLIENT_TELEGRAM_API_KEY" ]; then echo -e " Клиентский бот: ${GREEN}✅ настроен${NC}" else echo -e " Клиентский бот: ${YELLOW}⚠ отключен${NC}" fi echo "" echo -e "${WHITE}💾 Хранилище и конвертация:${NC}" echo -e " Путь хранения: ${YELLOW}$STORAGE_DIR${NC}" echo -e " Docker socket: ${YELLOW}$DOCKER_SOCK_PATH${NC}" if [ -S "$DOCKER_SOCK_PATH" ]; then echo -e " Converter: ${GREEN}✅ готов к работе${NC}" # Проверка наличия converter образа if docker images | grep -q "my-network-converter"; then echo -e " Converter образ: ${GREEN}✅ my-network-converter:latest${NC}" else echo -e " Converter образ: ${YELLOW}⚠ не найден${NC}" fi else echo -e " Converter: ${RED}❌ Docker socket недоступен${NC}" fi echo "" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE} КОМАНДЫ УПРАВЛЕНИЯ ${NC}" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo -e "${WHITE}🔧 Управление сервисом:${NC}" echo -e " ${BLUE}systemctl start my-network${NC} # Запуск" echo -e " ${BLUE}systemctl stop my-network${NC} # Остановка" echo -e " ${BLUE}systemctl restart my-network${NC} # Перезапуск" echo -e " ${BLUE}systemctl status my-network${NC} # Статус" echo "" echo -e "${WHITE}📊 Мониторинг:${NC}" echo -e " ${BLUE}$COMPOSE_CMD -f $PROJECT_DIR/my-network/docker-compose.yml logs -f${NC}" echo -e " ${BLUE}curl http://localhost:8000/api/v3/node/status | jq${NC}" echo -e " ${BLUE}curl http://localhost:8000/api/v3/network/stats | jq${NC}" echo "" echo -e "${WHITE}📁 Важные файлы:${NC}" echo -e " Конфигурация: ${YELLOW}$CONFIG_DIR/.env${NC}" echo -e " Bootstrap: ${YELLOW}$CONFIG_DIR/bootstrap.json${NC}" echo -e " Логи: ${YELLOW}$LOGS_DIR/${NC}" echo -e " Проект: ${YELLOW}$PROJECT_DIR/my-network/${NC}" echo "" echo -e "${GREEN}🎉 MY Network v3.0 успешно установлен и запущен!${NC}" echo "" echo -e "${WHITE}🔐 Криптографическая безопасность:${NC}" echo -e " Node ID: ${YELLOW}$NODE_ID${NC}" echo -e " Приватный ключ: ${YELLOW}$CONFIG_DIR/keys/node_private_key${NC}" echo -e " Публичный ключ: ${YELLOW}$CONFIG_DIR/keys/node_public_key${NC}" echo -e " Ed25519 ключ: ${GREEN}✅ сгенерирован и защищен${NC}" echo -e " Подписи: ${GREEN}✅ все соединения подписываются ed25519${NC}" echo "" echo -e "${WHITE}Особенности v3.0:${NC}" echo -e " ✅ Полная децентрализация без консенсуса" echo -e " ✅ Мгновенная трансляция контента" echo -e " ✅ Автоматическая конвертация через Docker" echo -e " ✅ Блокчейн интеграция для uploader-bot" echo -e " ✅ Поддержка приватных и публичных нод" echo -e " ✅ Ed25519 криптографическая идентификация" echo -e " ✅ Подписанные и проверенные соединения" echo "" # Сохранение отчета cat > "$PROJECT_DIR/installation-report.txt" << EOF MY Network v3.0 Installation Report Generated: $(date) Node Configuration: - Node ID: $NODE_ID - Node Type: $NODE_TYPE - Network Mode: $NETWORK_MODE - Version: $NODE_VERSION - Allow Incoming: $ALLOW_INCOMING Paths: - Project: $PROJECT_DIR/my-network - Storage: $STORAGE_DIR - Config: $CONFIG_DIR - Logs: $LOGS_DIR - Docker Socket: $DOCKER_SOCK_PATH API Endpoints: - Health: http://localhost:8000/health - Node Status: http://localhost:8000/api/v3/node/status - Network Stats: http://localhost:8000/api/v3/network/stats - Monitoring: http://localhost:8000/api/my/monitor/ Management Commands: - Start: systemctl start my-network - Stop: systemctl stop my-network - Status: systemctl status my-network - Logs: $COMPOSE_CMD -f $PROJECT_DIR/my-network/docker-compose.yml logs -f Telegram Bots: - Main Bot: $([ -n "$TELEGRAM_API_KEY" ] && echo "enabled" || echo "disabled") - Client Bot: $([ -n "$CLIENT_TELEGRAM_API_KEY" ] && echo "enabled" || echo "disabled") EOF log_success "Отчет об установке сохранен: $PROJECT_DIR/installation-report.txt" } # Основная функция main() { show_banner check_root detect_os check_existing_installation interactive_setup install_dependencies install_docker create_directories setup_project patch_app_routes add_compatibility_layer build_converter_image setup_nginx generate_config setup_firewall build_and_start init_database connect_to_network create_systemd_service final_report } # Обработка ошибок error_handler() { set +e log_error "Ошибка на строке $1. Код выхода: $2" log_error "Установка прервана" # Показать логи для диагностики if [ -d "$PROJECT_DIR/my-network" ]; then log_info "Логи для диагностики:" cd "$PROJECT_DIR/my-network" init_compose_cmd if [ -n "$COMPOSE_CMD" ]; then $COMPOSE_CMD logs --tail=50 || true fi fi exit $2 } trap 'error_handler $LINENO $?' ERR # Запуск установки main "$@"